Complete overhaul
* Switch to Python 2.x due to lack of decent LDAP support in Python 3.x * Add LDAP backend for authentication/authorization * Add PAM backend for authentication * Add getent backend for authorization * Add preliminary CSRF protection * Update icons * Update push server documentation, use nchan from now on * Add P12 bundle generation * Add thin wrapper around Python's SQL connectors * Enable mailing subsystem * Add Kerberos TGT renewal cronjob * Add HTTPS server setup commands for nginx
6
.gitignore
vendored
@ -54,3 +54,9 @@ docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# npm
|
||||
node_modules/
|
||||
|
||||
# diff
|
||||
*.diff
|
||||
|
@ -1,13 +1,11 @@
|
||||
include README.rst
|
||||
include certidude/templates/*.sh
|
||||
include certidude/templates/*.html
|
||||
include certidude/templates/*.svg
|
||||
include certidude/templates/*.ovpn
|
||||
include certidude/templates/*.cnf
|
||||
include certidude/templates/*.conf
|
||||
include certidude/templates/*.ini
|
||||
include certidude/templates/mail/*.md
|
||||
include certidude/static/js/*.js
|
||||
include certidude/static/css/*.css
|
||||
include certidude/static/fonts/*.woff2
|
||||
include certidude/static/img/*.svg
|
||||
include certidude/static/*.html
|
||||
include certidude/sql/*/*.sql
|
||||
|
77
README.rst
@ -67,8 +67,11 @@ To install Certidude:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
apt-get install -y python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev libkrb5-dev
|
||||
pip3 install --allow-external mysql-connector-python mysql-connector-python
|
||||
apt-get install -y python python-pip python-dev cython \
|
||||
python-pysqlite2 python-mysql.connector python-ldap \
|
||||
build-essential libffi-dev libssl-dev libkrb5-dev \
|
||||
ldap-utils krb5-user default-mta \
|
||||
libsasl2-modules-gssapi-mit
|
||||
pip3 install certidude
|
||||
|
||||
Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI,
|
||||
@ -79,6 +82,7 @@ Create a system user for ``certidude``:
|
||||
.. code:: bash
|
||||
|
||||
adduser --system --no-create-home --group certidude
|
||||
mkdir /etc/certidude
|
||||
|
||||
|
||||
Setting up CA
|
||||
@ -90,7 +94,7 @@ You can check it with:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
hostname -f
|
||||
hostname -f
|
||||
|
||||
The command should return ca.example.co
|
||||
|
||||
@ -144,7 +148,7 @@ Install ``nginx`` and ``uwsgi``:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
apt-get install nginx uwsgi uwsgi-plugin-python3
|
||||
apt-get install nginx uwsgi uwsgi-plugin-python
|
||||
|
||||
For easy setup following is reccommended:
|
||||
|
||||
@ -162,7 +166,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl
|
||||
vaccum = true
|
||||
uid = certidude
|
||||
gid = certidude
|
||||
plugins = python34
|
||||
plugins = python
|
||||
chdir = /tmp
|
||||
module = certidude.wsgi
|
||||
callable = app
|
||||
@ -192,7 +196,7 @@ configure the site in /etc/nginx/sites-available/certidude:
|
||||
server_name localhost;
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server ipv6only=on;
|
||||
root /usr/local/lib/python3.4/dist-packages/certidude/static;
|
||||
root /usr/local/lib/python2.7/dist-packages/certidude/static;
|
||||
|
||||
location /api/ {
|
||||
include uwsgi_params;
|
||||
@ -201,19 +205,20 @@ configure the site in /etc/nginx/sites-available/certidude:
|
||||
|
||||
# Add following three if you wish to enable push server on this machine
|
||||
location /pub {
|
||||
allow 127.0.0.1; # Allow publishing only from CA machine
|
||||
push_stream_publisher admin;
|
||||
push_stream_channels_path $arg_id;
|
||||
allow 127.0.0.1;
|
||||
nchan_publisher http;
|
||||
nchan_store_messages off;
|
||||
nchan_channel_id $arg_id;
|
||||
}
|
||||
|
||||
location ~ "^/lp/(.*)" {
|
||||
push_stream_channels_path $1;
|
||||
push_stream_subscriber long-polling;
|
||||
nchan_subscriber longpoll;
|
||||
nchan_channel_id $1;
|
||||
}
|
||||
|
||||
location ~ "^/ev/(.*)" {
|
||||
push_stream_channels_path $1;
|
||||
push_stream_subscriber eventsource;
|
||||
nchan_subscriber eventsource;
|
||||
nchan_channel_id $1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,6 +259,8 @@ Also adjust ``/etc/nginx/nginx.conf``:
|
||||
|
||||
In your CA ssl.cnf make sure Certidude is aware of your nginx setup:
|
||||
|
||||
.. code::
|
||||
|
||||
push_server = http://push.example.com/
|
||||
|
||||
Restart the services:
|
||||
@ -283,7 +290,7 @@ Make sure Certidude machine's fully qualified hostname is correct in ``/etc/host
|
||||
127.0.0.1 localhost
|
||||
127.0.1.1 ca.example.lan ca
|
||||
|
||||
Set up Samba client configuration in ``/etc/samba/smb.conf``:
|
||||
Reset Samba client configuration in ``/etc/samba/smb.conf``:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
@ -294,11 +301,36 @@ Set up Samba client configuration in ``/etc/samba/smb.conf``:
|
||||
realm = EXAMPLE.LAN
|
||||
kerberos method = system keytab
|
||||
|
||||
Reset Kerberos configuration in ``/etc/krb5.conf``:
|
||||
|
||||
.. code:: ini
|
||||
|
||||
[libdefaults]
|
||||
default_realm = EXAMPLE.LAN
|
||||
dns_lookup_realm = true
|
||||
dns_lookup_kdc = true
|
||||
forwardable = true
|
||||
proxiable = true
|
||||
|
||||
Initialize Kerberos credentials:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
kinit administrator
|
||||
|
||||
Join the machine to domain:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
net ads join -k
|
||||
|
||||
Set up Kerberos keytab for the web service:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -U Administrator
|
||||
KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k
|
||||
chown root:certidude /etc/certidude/server.keytab
|
||||
chmod 640 /etc/certidude/server.keytab
|
||||
|
||||
|
||||
Setting up authorization
|
||||
@ -379,22 +411,29 @@ Clone the repository:
|
||||
git clone https://github.com/laurivosandi/certidude
|
||||
cd certidude
|
||||
|
||||
Install dependencies as shown above and additionally:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
To generate templates:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
apt-get install npm nodejs
|
||||
npm install nunjucks
|
||||
nunjucks-precompile --include "\\.html$" --include "\\.svg" certidude/static/ > certidude/static/js/templates.js
|
||||
sudo ln -s nodejs /usr/bin/node # Fix 'env node' on Ubuntu 14.04
|
||||
npm install -g nunjucks
|
||||
nunjucks-precompile --include "\\.html$" --include "\\.svg$" certidude/static/ > certidude/static/js/templates.js
|
||||
|
||||
To run from source tree:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python3 misc/certidude
|
||||
PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python misc/certidude
|
||||
|
||||
To install the package from the source:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python3 setup.py install --single-version-externally-managed --root /
|
||||
python setup.py install --single-version-externally-managed --root /
|
||||
|
@ -1,13 +1,19 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import falcon
|
||||
import mimetypes
|
||||
import logging
|
||||
import os
|
||||
import click
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
from certidude import authority
|
||||
from certidude import authority, mailer
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import serialize, event_source
|
||||
from certidude.decorators import serialize, event_source, csrf_protection
|
||||
from certidude.wrappers import Request, Certificate
|
||||
from certidude import config
|
||||
from certidude import constants, config
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
class CertificateStatusResource(object):
|
||||
"""
|
||||
@ -24,7 +30,9 @@ class CertificateStatusResource(object):
|
||||
|
||||
class CertificateAuthorityResource(object):
|
||||
def on_get(self, req, resp):
|
||||
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
|
||||
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
|
||||
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||
resp.append_header("Content-Disposition", "attachment; filename=ca.crt")
|
||||
|
||||
|
||||
@ -34,16 +42,54 @@ class SessionResource(object):
|
||||
@authorize_admin
|
||||
@event_source
|
||||
def on_get(self, req, resp):
|
||||
if config.ACCOUNTS_BACKEND == "ldap":
|
||||
import ldap
|
||||
ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*")
|
||||
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE,
|
||||
ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"])
|
||||
|
||||
for dn,entry in r:
|
||||
cn, = entry.get("cn")
|
||||
break
|
||||
else:
|
||||
raise ValueError("Failed to look up group %s in LDAP" % repr(group_name))
|
||||
|
||||
admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")])
|
||||
elif config.ACCOUNTS_BACKEND == "posix":
|
||||
import grp
|
||||
_, _, gid, members = grp.getgrnam(config.ADMINS_GROUP)
|
||||
admins = dict([(j, j) for j in members])
|
||||
else:
|
||||
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
|
||||
|
||||
return dict(
|
||||
username=req.context.get("user"),
|
||||
event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN,
|
||||
user = dict(
|
||||
name=req.context.get("user").name,
|
||||
gn=req.context.get("user").given_name,
|
||||
sn=req.context.get("user").surname,
|
||||
mail=req.context.get("user").mail
|
||||
),
|
||||
request_submission_allowed = sum( # Dirty hack!
|
||||
[req.context.get("remote_addr") in j
|
||||
for j in config.REQUEST_SUBNETS]),
|
||||
user_subnets = config.USER_SUBNETS,
|
||||
autosign_subnets = config.AUTOSIGN_SUBNETS,
|
||||
request_subnets = config.REQUEST_SUBNETS,
|
||||
admin_subnets=config.ADMIN_SUBNETS,
|
||||
admin_users=config.ADMIN_USERS,
|
||||
requests=authority.list_requests(),
|
||||
signed=authority.list_signed(),
|
||||
revoked=authority.list_revoked())
|
||||
admin_users = admins,
|
||||
#admin_users=config.ADMIN_USERS,
|
||||
authority = dict(
|
||||
outbox = config.OUTBOX,
|
||||
certificate = authority.certificate,
|
||||
events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN,
|
||||
requests=authority.list_requests(),
|
||||
signed=authority.list_signed(),
|
||||
revoked=authority.list_revoked(),
|
||||
) if config.ADMINS_GROUP in req.context.get("groups") else None,
|
||||
features=dict(
|
||||
tagging=config.TAGGING_BACKEND,
|
||||
leases=False, #config.LEASES_BACKEND,
|
||||
logging=config.LOGGING_BACKEND))
|
||||
|
||||
|
||||
class StaticResource(object):
|
||||
@ -58,7 +104,7 @@ class StaticResource(object):
|
||||
|
||||
if os.path.isdir(path):
|
||||
path = os.path.join(path, "index.html")
|
||||
print("Serving:", path)
|
||||
click.echo("Serving: %s" % path)
|
||||
|
||||
if os.path.exists(path):
|
||||
content_type, content_encoding = mimetypes.guess_type(path)
|
||||
@ -72,7 +118,33 @@ class StaticResource(object):
|
||||
resp.body = "File '%s' not found" % req.path
|
||||
|
||||
|
||||
class BundleResource(object):
|
||||
@login_required
|
||||
def on_get(self, req, resp):
|
||||
common_name = req.context["user"].mail
|
||||
logger.info("Signing bundle %s for %s", common_name, req.context.get("user"))
|
||||
resp.set_header("Content-Type", "application/x-pkcs12")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name)
|
||||
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
|
||||
|
||||
import ipaddress
|
||||
|
||||
class NormalizeMiddleware(object):
|
||||
@csrf_protection
|
||||
def process_request(self, req, resp, *args):
|
||||
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
||||
req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8"))
|
||||
|
||||
def process_response(self, req, resp, resource):
|
||||
# wtf falcon?!
|
||||
if isinstance(resp.location, unicode):
|
||||
resp.location = resp.location.encode("ascii")
|
||||
|
||||
def certidude_app():
|
||||
from certidude import config
|
||||
|
||||
from .revoked import RevocationListResource
|
||||
from .signed import SignedCertificateListResource, SignedCertificateDetailResource
|
||||
from .request import RequestListResource, RequestDetailResource
|
||||
@ -82,60 +154,56 @@ def certidude_app():
|
||||
from .tag import TagResource, TagDetailResource
|
||||
from .cfg import ConfigResource, ScriptResource
|
||||
|
||||
app = falcon.API()
|
||||
app = falcon.API(middleware=NormalizeMiddleware())
|
||||
|
||||
# Certificate authority API calls
|
||||
app.add_route("/api/ocsp/", CertificateStatusResource())
|
||||
app.add_route("/api/bundle/", BundleResource())
|
||||
app.add_route("/api/certificate/", CertificateAuthorityResource())
|
||||
app.add_route("/api/revoked/", RevocationListResource())
|
||||
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())
|
||||
app.add_route("/api/signed/", SignedCertificateListResource())
|
||||
app.add_route("/api/request/{cn}/", RequestDetailResource())
|
||||
app.add_route("/api/request/", RequestListResource())
|
||||
app.add_route("/api/log/", LogResource())
|
||||
app.add_route("/api/tag/", TagResource())
|
||||
app.add_route("/api/tag/{identifier}/", TagDetailResource())
|
||||
app.add_route("/api/config/", ConfigResource())
|
||||
app.add_route("/api/script/", ScriptResource())
|
||||
app.add_route("/api/", SessionResource())
|
||||
|
||||
# Gateway API calls, should this be moved to separate project?
|
||||
app.add_route("/api/lease/", LeaseResource())
|
||||
app.add_route("/api/whois/", WhoisResource())
|
||||
|
||||
"""
|
||||
Set up logging
|
||||
"""
|
||||
log_handlers = []
|
||||
if config.LOGGING_BACKEND == "sql":
|
||||
from certidude.mysqllog import LogHandler
|
||||
uri = config.cp.get("logging", "database")
|
||||
log_handlers.append(LogHandler(uri))
|
||||
app.add_route("/api/log/", LogResource(uri))
|
||||
elif config.LOGGING_BACKEND == "syslog":
|
||||
from logging.handlers import SyslogHandler
|
||||
log_handlers.append(SysLogHandler())
|
||||
# Browsing syslog via HTTP is obviously not possible out of the box
|
||||
elif config.LOGGING_BACKEND:
|
||||
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
|
||||
|
||||
from certidude import config
|
||||
from certidude.mysqllog import MySQLLogHandler
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import socket
|
||||
import json
|
||||
if config.TAGGING_BACKEND == "sql":
|
||||
uri = config.cp.get("tagging", "database")
|
||||
app.add_route("/api/tag/", TagResource(uri))
|
||||
app.add_route("/api/tag/{identifier}/", TagDetailResource(uri))
|
||||
app.add_route("/api/config/", ConfigResource(uri))
|
||||
app.add_route("/api/script/", ScriptResource(uri))
|
||||
elif config.TAGGING_BACKEND:
|
||||
raise ValueError("Invalid tagging.backend = %s" % config.TAGGING_BACKEND)
|
||||
|
||||
|
||||
class PushLogHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
from certidude.push import publish
|
||||
publish("log-entry", dict(
|
||||
created = datetime.fromtimestamp(record.created),
|
||||
message = record.msg % record.args,
|
||||
severity = record.levelname.lower()))
|
||||
|
||||
if config.DATABASE_POOL:
|
||||
sql_handler = MySQLLogHandler(config.DATABASE_POOL)
|
||||
push_handler = PushLogHandler()
|
||||
if config.PUSH_PUBLISH:
|
||||
from certidude.push import PushLogHandler
|
||||
log_handlers.append(PushLogHandler())
|
||||
|
||||
for facility in "api", "cli":
|
||||
logger = logging.getLogger(facility)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if config.DATABASE_POOL:
|
||||
logger.addHandler(sql_handler)
|
||||
logger.addHandler(push_handler)
|
||||
for handler in log_handlers:
|
||||
logger.addHandler(handler)
|
||||
|
||||
|
||||
logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN)
|
||||
logging.getLogger("cli").debug("Started Certidude at %s", constants.FQDN)
|
||||
|
||||
import atexit
|
||||
|
||||
|
@ -6,6 +6,7 @@ from random import choice
|
||||
from certidude import config
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import serialize
|
||||
from certidude.relational import RelationalMixin
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
@ -39,43 +40,42 @@ where
|
||||
device.cn = %s
|
||||
"""
|
||||
|
||||
SQL_SELECT_INHERITANCE = """
|
||||
|
||||
SQL_SELECT_RULES = """
|
||||
select
|
||||
tag_inheritance.`id` as `id`,
|
||||
tag.id as `tag_id`,
|
||||
tag.`key` as `match_key`,
|
||||
tag.`value` as `match_value`,
|
||||
tag_inheritance.`key` as `key`,
|
||||
tag_inheritance.`value` as `value`
|
||||
from tag_inheritance
|
||||
join tag on tag.id = tag_inheritance.tag_id
|
||||
tag.cn as `cn`,
|
||||
tag.key as `tag_key`,
|
||||
tag.value as `tag_value`,
|
||||
tag_properties.property_key as `property_key`,
|
||||
tag_properties.property_value as `property_value`
|
||||
from
|
||||
tag_properties
|
||||
join
|
||||
tag
|
||||
on
|
||||
tag.key = tag_properties.tag_key and
|
||||
tag.value = tag_properties.tag_value
|
||||
"""
|
||||
|
||||
class ConfigResource(object):
|
||||
|
||||
class ConfigResource(RelationalMixin):
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp):
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(SQL_SELECT_INHERITANCE)
|
||||
def g():
|
||||
for row in cursor:
|
||||
yield row
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return g()
|
||||
return self.iterfetch(SQL_SELECT_RULES)
|
||||
|
||||
class ScriptResource(object):
|
||||
|
||||
class ScriptResource(RelationalMixin):
|
||||
def on_get(self, req, resp):
|
||||
from certidude.api.whois import address_to_identity
|
||||
|
||||
node = address_to_identity(
|
||||
config.DATABASE_POOL.get_connection(),
|
||||
ipaddress.ip_address(req.env["REMOTE_ADDR"])
|
||||
self.connect(),
|
||||
req.context.get("remote_addr")
|
||||
)
|
||||
if not node:
|
||||
resp.body = "Could not map IP address: %s" % req.env["REMOTE_ADDR"]
|
||||
resp.body = "Could not map IP address: %s" % req.context.get("remote_addr")
|
||||
resp.status = falcon.HTTP_404
|
||||
return
|
||||
|
||||
@ -84,7 +84,7 @@ class ScriptResource(object):
|
||||
key, common_name = identity.split("=")
|
||||
assert "=" not in common_name
|
||||
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
conn = self.connect()
|
||||
cursor = conn.cursor()
|
||||
|
||||
resp.set_header("Content-Type", "text/x-shellscript")
|
||||
|
@ -2,38 +2,14 @@
|
||||
from certidude import config
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import serialize
|
||||
from certidude.relational import RelationalMixin
|
||||
|
||||
class LogResource(RelationalMixin):
|
||||
SQL_CREATE_TABLES = "log_tables.sql"
|
||||
|
||||
class LogResource(object):
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp):
|
||||
"""
|
||||
Translate currently online client's IP-address to distinguished name
|
||||
"""
|
||||
|
||||
SQL_LOG_ENTRIES = """
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
log
|
||||
ORDER BY created DESC
|
||||
"""
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(SQL_LOG_ENTRIES)
|
||||
|
||||
def g():
|
||||
for row in cursor:
|
||||
yield row
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return tuple(g())
|
||||
|
||||
# for acquired, released, identity in cursor:
|
||||
# return {
|
||||
# "acquired": datetime.utcfromtimestamp(acquired),
|
||||
# "identity": parse_dn(bytes(identity))
|
||||
# }
|
||||
# return None
|
||||
|
||||
# TODO: Add last id parameter
|
||||
return self.iterfetch("select * from log order by created desc")
|
||||
|
@ -5,45 +5,42 @@ import logging
|
||||
import ipaddress
|
||||
import os
|
||||
from certidude import config, authority, helpers, push, errors
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.auth import login_required, login_optional, authorize_admin
|
||||
from certidude.decorators import serialize
|
||||
from certidude.wrappers import Request, Certificate
|
||||
from certidude.firewall import whitelist_subnets, whitelist_content_types
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
class RequestListResource(object):
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp):
|
||||
return helpers.list_requests()
|
||||
return authority.list_requests()
|
||||
|
||||
@login_optional
|
||||
@whitelist_subnets(config.REQUEST_SUBNETS)
|
||||
@whitelist_content_types("application/pkcs10")
|
||||
def on_post(self, req, resp):
|
||||
"""
|
||||
Submit certificate signing request (CSR) in PEM format
|
||||
"""
|
||||
# Parse remote IPv4/IPv6 address
|
||||
remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8"))
|
||||
|
||||
# Check for CSR submission whitelist
|
||||
if config.REQUEST_SUBNETS:
|
||||
for subnet in config.REQUEST_SUBNETS:
|
||||
if subnet.overlaps(remote_addr):
|
||||
break
|
||||
else:
|
||||
logger.warning("Attempted to submit signing request from non-whitelisted address %s", remote_addr)
|
||||
raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr)
|
||||
|
||||
if req.get_header("Content-Type") != "application/pkcs10":
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"This API call accepts only application/pkcs10 content type")
|
||||
|
||||
body = req.stream.read(req.content_length).decode("ascii")
|
||||
body = req.stream.read(req.content_length)
|
||||
csr = Request(body)
|
||||
|
||||
if not csr.common_name:
|
||||
logger.warning("Rejected signing request without common name from %s",
|
||||
req.context.get("remote_addr"))
|
||||
raise falcon.HTTPBadRequest(
|
||||
"Bad request",
|
||||
"No common name specified!")
|
||||
|
||||
# Check if this request has been already signed and return corresponding certificte if it has been signed
|
||||
try:
|
||||
cert = authority.get_signed(csr.common_name)
|
||||
except FileNotFoundError:
|
||||
except EnvironmentError:
|
||||
pass
|
||||
else:
|
||||
if cert.pubkey == csr.pubkey:
|
||||
@ -56,12 +53,12 @@ class RequestListResource(object):
|
||||
# Process automatic signing if the IP address is whitelisted and autosigning was requested
|
||||
if req.get_param_as_bool("autosign"):
|
||||
for subnet in config.AUTOSIGN_SUBNETS:
|
||||
if subnet.overlaps(remote_addr):
|
||||
if subnet.overlaps(req.context.get("remote_addr")):
|
||||
try:
|
||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||
resp.body = authority.sign(csr).dump()
|
||||
return
|
||||
except FileExistsError: # Certificate already exists, try to save the request
|
||||
except EnvironmentError: # Certificate already exists, try to save the request
|
||||
pass
|
||||
break
|
||||
|
||||
@ -73,7 +70,8 @@ class RequestListResource(object):
|
||||
pass
|
||||
except errors.DuplicateCommonNameError:
|
||||
# TODO: Certificate renewal
|
||||
logger.warning("Rejected signing request with overlapping common name from %s", req.env["REMOTE_ADDR"])
|
||||
logger.warning("Rejected signing request with overlapping common name from %s",
|
||||
req.context.get("remote_addr"))
|
||||
raise falcon.HTTPConflict(
|
||||
"CSR with such CN already exists",
|
||||
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
||||
@ -86,12 +84,12 @@ class RequestListResource(object):
|
||||
url = config.PUSH_LONG_POLL % csr.fingerprint()
|
||||
click.echo("Redirecting to: %s" % url)
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.set_header("Location", url)
|
||||
logger.warning("Redirecting signing request from %s to %s", req.env["REMOTE_ADDR"], url)
|
||||
resp.set_header("Location", url.encode("ascii"))
|
||||
logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
|
||||
else:
|
||||
# Request was accepted, but not processed
|
||||
resp.status = falcon.HTTP_202
|
||||
logger.info("Signing request from %s stored", req.env["REMOTE_ADDR"])
|
||||
logger.info("Signing request from %s stored", req.context.get("remote_addr"))
|
||||
|
||||
|
||||
class RequestDetailResource(object):
|
||||
@ -101,11 +99,8 @@ class RequestDetailResource(object):
|
||||
Fetch certificate signing request as PEM
|
||||
"""
|
||||
csr = authority.get_request(cn)
|
||||
# if not os.path.exists(path):
|
||||
# raise falcon.HTTPNotFound()
|
||||
|
||||
resp.set_header("Content-Type", "application/pkcs10")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name)
|
||||
logger.debug("Signing request %s was downloaded by %s",
|
||||
csr.common_name, req.context.get("remote_addr"))
|
||||
return csr
|
||||
|
||||
@login_required
|
||||
@ -120,14 +115,17 @@ class RequestDetailResource(object):
|
||||
resp.body = "Certificate successfully signed"
|
||||
resp.status = falcon.HTTP_201
|
||||
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
||||
logger.info("Signing request %s signed by %s from %s", csr.common_name, req.context["user"], req.env["REMOTE_ADDR"])
|
||||
logger.info("Signing request %s signed by %s from %s", csr.common_name,
|
||||
req.context.get("user"), req.context.get("remote_addr"))
|
||||
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_delete(self, req, resp, cn):
|
||||
try:
|
||||
authority.delete_request(cn)
|
||||
except FileNotFoundError:
|
||||
# Logging implemented in the function above
|
||||
except EnvironmentError as e:
|
||||
resp.body = "No certificate CN=%s found" % cn
|
||||
logger.warning("User %s attempted to delete non-existant signing request %s from %s", req.context["user"], cn, req.env["REMOTE_ADDR"])
|
||||
logger.warning("User %s failed to delete signing request %s from %s, reason: %s",
|
||||
req.context["user"], cn, req.context.get("remote_addr"), e)
|
||||
raise falcon.HTTPNotFound()
|
||||
|
@ -1,9 +1,12 @@
|
||||
|
||||
import logging
|
||||
from certidude.authority import export_crl
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
class RevocationListResource(object):
|
||||
def on_get(self, req, resp):
|
||||
logger.debug("Revocation list requested by %s", req.context.get("remote_addr"))
|
||||
resp.set_header("Content-Type", "application/x-pkcs7-crl")
|
||||
resp.append_header("Content-Disposition", "attachment; filename=ca.crl")
|
||||
resp.body = export_crl()
|
||||
|
||||
|
@ -9,40 +9,35 @@ logger = logging.getLogger("api")
|
||||
|
||||
class SignedCertificateListResource(object):
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp):
|
||||
for j in authority.list_signed():
|
||||
yield omit(
|
||||
key_type=j.key_type,
|
||||
key_length=j.key_length,
|
||||
identity=j.identity,
|
||||
cn=j.common_name,
|
||||
c=j.country_code,
|
||||
st=j.state_or_county,
|
||||
l=j.city,
|
||||
o=j.organization,
|
||||
ou=j.organizational_unit,
|
||||
fingerprint=j.fingerprint())
|
||||
return {"signed":authority.list_signed()}
|
||||
|
||||
|
||||
class SignedCertificateDetailResource(object):
|
||||
@serialize
|
||||
def on_get(self, req, resp, cn):
|
||||
# Compensate for NTP lag
|
||||
from time import sleep
|
||||
sleep(5)
|
||||
# from time import sleep
|
||||
# sleep(5)
|
||||
try:
|
||||
logger.info("Served certificate %s to %s", cn, req.env["REMOTE_ADDR"])
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
|
||||
return authority.get_signed(cn)
|
||||
except FileNotFoundError:
|
||||
logger.warning("Failed to serve non-existant certificate %s to %s", cn, req.env["REMOTE_ADDR"])
|
||||
cert = authority.get_signed(cn)
|
||||
except EnvironmentError:
|
||||
logger.warning("Failed to serve non-existant certificate %s to %s",
|
||||
cn, req.context.get("remote_addr"))
|
||||
resp.body = "No certificate CN=%s found" % cn
|
||||
raise falcon.HTTPNotFound()
|
||||
else:
|
||||
logger.debug("Served certificate %s to %s",
|
||||
cn, req.context.get("remote_addr"))
|
||||
return cert
|
||||
|
||||
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_delete(self, req, resp, cn):
|
||||
logger.info("Revoked certificate %s by %s from %s", cn, req.context["user"], req.env["REMOTE_ADDR"])
|
||||
logger.info("Revoked certificate %s by %s from %s",
|
||||
cn, req.context.get("user"), req.context.get("remote_addr"))
|
||||
authority.revoke_certificate(cn)
|
||||
|
||||
|
@ -1,117 +1,63 @@
|
||||
|
||||
import falcon
|
||||
import logging
|
||||
from certidude import config
|
||||
from certidude.relational import RelationalMixin
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import serialize
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
SQL_TAG_LIST = """
|
||||
select
|
||||
device_tag.id as `id`,
|
||||
tag.key as `key`,
|
||||
tag.value as `value`,
|
||||
device.cn as `cn`
|
||||
from
|
||||
device_tag
|
||||
join
|
||||
tag
|
||||
on
|
||||
device_tag.tag_id = tag.id
|
||||
join
|
||||
device
|
||||
on
|
||||
device_tag.device_id = device.id
|
||||
"""
|
||||
class TagResource(RelationalMixin):
|
||||
SQL_CREATE_TABLES = "tag_tables.sql"
|
||||
|
||||
SQL_TAG_DETAIL = SQL_TAG_LIST + " where device_tag.id = %s"
|
||||
|
||||
class TagResource(object):
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp):
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(SQL_TAG_LIST)
|
||||
return self.iterfetch("select * from tag")
|
||||
|
||||
def g():
|
||||
for row in cursor:
|
||||
yield row
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return tuple(g())
|
||||
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_post(self, req, resp):
|
||||
from certidude import push
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
args = req.get_param("cn"),
|
||||
cursor.execute(
|
||||
"insert ignore device (`cn`) values (%s) on duplicate key update used = NOW();", args)
|
||||
device_id = cursor.lastrowid
|
||||
|
||||
args = req.get_param("key"), req.get_param("value")
|
||||
cursor.execute(
|
||||
"insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args)
|
||||
tag_id = cursor.lastrowid
|
||||
|
||||
args = device_id, tag_id
|
||||
cursor.execute(
|
||||
"insert into device_tag (`device_id`, `tag_id`) values (%s, %s);", args)
|
||||
|
||||
push.publish("tag-added", str(cursor.lastrowid))
|
||||
|
||||
args = req.get_param("cn"), req.get_param("key"), req.get_param("value")
|
||||
rowid = self.sql_execute("tag_insert.sql", *args)
|
||||
push.publish("tag-added", str(rowid))
|
||||
logger.debug("Tag cn=%s, key=%s, value=%s added" % args)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
class TagDetailResource(object):
|
||||
class TagDetailResource(RelationalMixin):
|
||||
SQL_CREATE_TABLES = "tag_tables.sql"
|
||||
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp, identifier):
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute(SQL_TAG_DETAIL, (identifier,))
|
||||
conn = self.sql_connect()
|
||||
cursor = conn.cursor()
|
||||
if self.uri.scheme == "mysql":
|
||||
cursor.execute("select `cn`, `key`, `value` from tag where id = %s", (identifier,))
|
||||
else:
|
||||
cursor.execute("select `cn`, `key`, `value` from tag where id = ?", (identifier,))
|
||||
cols = [j[0] for j in cursor.description]
|
||||
for row in cursor:
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return row
|
||||
return dict(zip(cols, row))
|
||||
cursor.close()
|
||||
conn.close()
|
||||
raise falcon.HTTPNotFound()
|
||||
|
||||
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_put(self, req, resp, identifier):
|
||||
from certidude import push
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create tag if necessary
|
||||
args = req.get_param("key"), req.get_param("value")
|
||||
cursor.execute(
|
||||
"insert into tag (`key`, `value`) values (%s, %s) on duplicate key update used = NOW();", args)
|
||||
tag_id = cursor.lastrowid
|
||||
|
||||
# Attach tag to device
|
||||
cursor.execute("update device_tag set tag_id = %s where `id` = %s limit 1",
|
||||
(tag_id, identifier))
|
||||
conn.commit()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
args = req.get_param("value"), identifier
|
||||
self.sql_execute("tag_update.sql", *args)
|
||||
logger.debug("Tag %s updated, value set to %s",
|
||||
identifier, req.get_param("value"))
|
||||
push.publish("tag-updated", identifier)
|
||||
@ -122,13 +68,6 @@ class TagDetailResource(object):
|
||||
@authorize_admin
|
||||
def on_delete(self, req, resp, identifier):
|
||||
from certidude import push
|
||||
conn = config.DATABASE_POOL.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("delete from device_tag where id = %s", (identifier,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
self.sql_execute("tag_delete.sql", identifier)
|
||||
push.publish("tag-removed", identifier)
|
||||
logger.debug("Tag %s removed" % identifier)
|
||||
|
||||
|
||||
|
@ -46,7 +46,7 @@ class WhoisResource(object):
|
||||
|
||||
identity = address_to_identity(
|
||||
conn,
|
||||
ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"])
|
||||
req.context.get("remote_addr")
|
||||
)
|
||||
|
||||
conn.close()
|
||||
@ -55,4 +55,4 @@ class WhoisResource(object):
|
||||
return dict(address=identity[0], acquired=identity[1], identity=identity[2])
|
||||
else:
|
||||
resp.status = falcon.HTTP_403
|
||||
resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"]
|
||||
resp.body = "Failed to look up node %s" % req.context.get("remote_addr")
|
||||
|
@ -1,144 +1,324 @@
|
||||
|
||||
import click
|
||||
import falcon
|
||||
import ipaddress
|
||||
import kerberos
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from certidude import config
|
||||
from certidude.firewall import whitelist_subnets
|
||||
from certidude import config, constants
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
# Vanilla Kerberos provides only username.
|
||||
# AD also embeds PAC (Privilege Attribute Certificate), which
|
||||
# is supposed to be sent via HTTP headers and it contains
|
||||
# the groups user is part of.
|
||||
# Even then we would have to manually look up the e-mail
|
||||
# address eg via LDAP, hence to keep things simple
|
||||
# we simply use Kerberos to authenticate.
|
||||
|
||||
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
|
||||
|
||||
if config.AUTHENTICATION_BACKEND == "kerberos":
|
||||
if not os.getenv("KRB5_KTNAME"):
|
||||
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
|
||||
ktname = os.getenv("KRB5_KTNAME")
|
||||
|
||||
if not ktname:
|
||||
click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True)
|
||||
exit(250)
|
||||
if not os.path.exists(ktname):
|
||||
click.echo("Kerberos keytab %s does not exist" % ktname, err=True)
|
||||
exit(248)
|
||||
|
||||
try:
|
||||
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
|
||||
except kerberos.KrbError as exc:
|
||||
click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True)
|
||||
click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True)
|
||||
exit(249)
|
||||
else:
|
||||
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
|
||||
else:
|
||||
NotImplemented
|
||||
|
||||
def login_required(func):
|
||||
def pam_authenticate(resource, req, resp, *args, **kwargs):
|
||||
"""
|
||||
Authenticate against PAM with WWW Basic Auth credentials
|
||||
"""
|
||||
authorization = req.get_header("Authorization")
|
||||
if not authorization:
|
||||
resp.append_header("WWW-Authenticate", "Basic")
|
||||
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate")
|
||||
|
||||
if not authorization.startswith("Basic "):
|
||||
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % authorization)
|
||||
class User(object):
|
||||
def __init__(self, name):
|
||||
if "@" in name:
|
||||
self.mail = name
|
||||
self.name, self.domain = name.split("@")
|
||||
else:
|
||||
self.mail = None
|
||||
self.name, self.domain = name, None
|
||||
self.given_name, self.surname = None, None
|
||||
|
||||
from base64 import b64decode
|
||||
basic, token = authorization.split(" ", 1)
|
||||
user, passwd = b64decode(token).split(":", 1)
|
||||
def __repr__(self):
|
||||
if self.given_name and self.surname:
|
||||
return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
|
||||
else:
|
||||
return self.mail
|
||||
|
||||
import simplepam
|
||||
if not simplepam.authenticate(user, passwd, "sshd"):
|
||||
raise falcon.HTTPForbidden("Forbidden", "Invalid password")
|
||||
|
||||
req.context["user"] = user
|
||||
def member_of(group_name):
|
||||
"""
|
||||
Check if requesting user is member of an UNIX group
|
||||
"""
|
||||
|
||||
def wrapper(func):
|
||||
def posix_check_group_membership(resource, req, resp, *args, **kwargs):
|
||||
import grp
|
||||
_, _, gid, members = grp.getgrnam(group_name)
|
||||
if req.context.get("user").name not in members:
|
||||
logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name)
|
||||
raise falcon.HTTPForbidden("Forbidden", "User not member of designated group")
|
||||
req.context.get("groups").add(group_name)
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
def ldap_check_group_membership(resource, req, resp, *args, **kwargs):
|
||||
import ldap
|
||||
|
||||
ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn)
|
||||
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
|
||||
ft.encode("utf-8"),
|
||||
["member"])
|
||||
|
||||
for dn,entry in r:
|
||||
if not dn: continue
|
||||
logger.debug("User %s is member of group %s" % (
|
||||
req.context.get("user"), repr(group_name)))
|
||||
req.context.get("groups").add(group_name)
|
||||
break
|
||||
else:
|
||||
raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name))
|
||||
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
if config.AUTHORIZATION_BACKEND == "ldap":
|
||||
return ldap_check_group_membership
|
||||
elif config.AUTHORIZATION_BACKEND == "posix":
|
||||
return posix_check_group_membership
|
||||
else:
|
||||
raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
|
||||
return wrapper
|
||||
|
||||
|
||||
def account_info(func):
|
||||
# TODO: Use Privilege Account Certificate for Kerberos
|
||||
|
||||
def posix_account_info(resource, req, resp, *args, **kwargs):
|
||||
import pwd
|
||||
_, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name)
|
||||
gecos = gecos.decode("utf-8").split(",")
|
||||
full_name = gecos[0]
|
||||
if full_name and " " in full_name:
|
||||
req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1)
|
||||
req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
def ldap_account_info(resource, req, resp, *args, **kwargs):
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
|
||||
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
|
||||
authorization = req.get_header("Authorization")
|
||||
if "ldap_conn" not in req.context:
|
||||
for server in config.LDAP_SERVERS:
|
||||
conn = ldap.initialize(server)
|
||||
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||
if os.path.exists("/etc/krb5.keytab"):
|
||||
ticket_cache = os.getenv("KRB5CCNAME")
|
||||
if not ticket_cache:
|
||||
raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!")
|
||||
click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache))
|
||||
conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
|
||||
else:
|
||||
raise NotImplementedError("LDAP simple bind not supported, use Kerberos")
|
||||
req.context["ldap_conn"] = conn
|
||||
break
|
||||
else:
|
||||
raise ValueError("No LDAP servers!")
|
||||
|
||||
if not authorization:
|
||||
resp.append_header("WWW-Authenticate", "Negotiate")
|
||||
logger.debug("No Kerberos ticket offered while attempting to access %s from %s", req.env["PATH_INFO"], req.env["REMOTE_ADDR"])
|
||||
raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?")
|
||||
ft = config.LDAP_USER_FILTER % req.context.get("user").name
|
||||
r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
|
||||
ft,
|
||||
["cn", "givenname", "sn", "mail", "userPrincipalName"])
|
||||
|
||||
token = ''.join(authorization.split()[1:])
|
||||
for dn, entry in r:
|
||||
if not dn: continue
|
||||
if entry.get("givenname") and entry.get("sn"):
|
||||
given_name, = entry.get("givenName")
|
||||
surname, = entry.get("sn")
|
||||
req.context["user"].given_name = given_name.decode("utf-8")
|
||||
req.context["user"].surname = surname.decode("utf-8")
|
||||
else:
|
||||
cn, = entry.get("cn")
|
||||
if " " in cn:
|
||||
req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1)
|
||||
|
||||
try:
|
||||
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
|
||||
except kerberos.GSSError as ex:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],))
|
||||
|
||||
try:
|
||||
result = kerberos.authGSSServerStep(context, token)
|
||||
except kerberos.GSSError as ex:
|
||||
s = str(dir(ex))
|
||||
kerberos.authGSSServerClean(context)
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s (%s)" % (ex.args[0][0], ex.args[1][0]))
|
||||
except kerberos.KrbError as ex:
|
||||
kerberos.authGSSServerClean(context)
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex.args[0],))
|
||||
|
||||
user = kerberos.authGSSServerUserName(context)
|
||||
req.context["user"], req.context["user_realm"] = user.split("@")
|
||||
|
||||
try:
|
||||
# BUGBUG: https://github.com/02strich/pykerberos/issues/6
|
||||
#kerberos.authGSSServerClean(context)
|
||||
pass
|
||||
except kerberos.GSSError as ex:
|
||||
# TODO: logger.error
|
||||
raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex.args[0][0], ex.args[1][0],))
|
||||
|
||||
if result == kerberos.AUTH_GSS_COMPLETE:
|
||||
logger.debug("Succesfully authenticated user %s for %s from %s", req.context["user"], req.env["PATH_INFO"], req.env["REMOTE_ADDR"])
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
elif result == kerberos.AUTH_GSS_CONTINUE:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
|
||||
req.context["user"].dn = dn.decode("utf-8")
|
||||
req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,)
|
||||
retval = func(resource, req, resp, *args, **kwargs)
|
||||
req.context.get("ldap_conn").unbind_s()
|
||||
return retval
|
||||
else:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
|
||||
raise ValueError("Failed to look up %s in LDAP" % req.context.get("user"))
|
||||
|
||||
if config.AUTHENTICATION_BACKEND == "kerberos":
|
||||
return kerberos_authenticate
|
||||
elif config.AUTHENTICATION_BACKEND == "pam":
|
||||
return pam_authenticate
|
||||
if config.ACCOUNTS_BACKEND == "ldap":
|
||||
return ldap_account_info
|
||||
elif config.ACCOUNTS_BACKEND == "posix":
|
||||
return posix_account_info
|
||||
else:
|
||||
NotImplemented
|
||||
raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND)
|
||||
|
||||
|
||||
def authenticate(optional=False):
|
||||
def wrapper(func):
|
||||
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
|
||||
if optional and not req.get_param_as_bool("authenticate"):
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
if not req.auth:
|
||||
resp.append_header("WWW-Authenticate", "Negotiate")
|
||||
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
||||
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||
"No Kerberos ticket offered, are you sure you've logged in with domain user account?")
|
||||
|
||||
token = ''.join(req.auth.split()[1:])
|
||||
|
||||
try:
|
||||
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
|
||||
except kerberos.GSSError as ex:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden",
|
||||
"Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],))
|
||||
|
||||
try:
|
||||
result = kerberos.authGSSServerStep(context, token)
|
||||
except kerberos.GSSError as ex:
|
||||
kerberos.authGSSServerClean(context)
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden",
|
||||
"Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1]))
|
||||
except kerberos.KrbError as ex:
|
||||
kerberos.authGSSServerClean(context)
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden",
|
||||
"Bad credentials: %s" % (ex.args[0],))
|
||||
|
||||
user = kerberos.authGSSServerUserName(context)
|
||||
req.context["user"] = User(user)
|
||||
req.context["groups"] = set()
|
||||
|
||||
try:
|
||||
kerberos.authGSSServerClean(context)
|
||||
except kerberos.GSSError as ex:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0]))
|
||||
|
||||
if result == kerberos.AUTH_GSS_COMPLETE:
|
||||
logger.debug("Succesfully authenticated user %s for %s from %s",
|
||||
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
|
||||
return account_info(func)(resource, req, resp, *args, **kwargs)
|
||||
elif result == kerberos.AUTH_GSS_CONTINUE:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
|
||||
else:
|
||||
# TODO: logger.error
|
||||
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
|
||||
|
||||
|
||||
def ldap_authenticate(resource, req, resp, *args, **kwargs):
|
||||
"""
|
||||
Authenticate against LDAP with WWW Basic Auth credentials
|
||||
"""
|
||||
|
||||
if optional and not req.get_param_as_bool("authenticate"):
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
import ldap
|
||||
|
||||
if not req.auth:
|
||||
resp.append_header("WWW-Authenticate", "Basic")
|
||||
raise falcon.HTTPUnauthorized("Forbidden",
|
||||
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
|
||||
|
||||
if not req.auth.startswith("Basic "):
|
||||
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
|
||||
|
||||
from base64 import b64decode
|
||||
basic, token = req.auth.split(" ", 1)
|
||||
user, passwd = b64decode(token).split(":", 1)
|
||||
|
||||
if "ldap_conn" not in req.context:
|
||||
for server in config.LDAP_SERVERS:
|
||||
click.echo("Connecting to %s as %s" % (server, user))
|
||||
conn = ldap.initialize(server)
|
||||
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||
try:
|
||||
conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd)
|
||||
except ldap.LDAPError, e:
|
||||
resp.append_header("WWW-Authenticate", "Basic")
|
||||
logger.debug("Failed to authenticate with user '%s'", user)
|
||||
raise falcon.HTTPUnauthorized("Forbidden",
|
||||
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
|
||||
|
||||
req.context["ldap_conn"] = conn
|
||||
break
|
||||
else:
|
||||
raise ValueError("No LDAP servers!")
|
||||
|
||||
req.context["user"] = User(user)
|
||||
req.context["groups"] = set()
|
||||
return account_info(func)(resource, req, resp, *args, **kwargs)
|
||||
|
||||
|
||||
def pam_authenticate(resource, req, resp, *args, **kwargs):
|
||||
"""
|
||||
Authenticate against PAM with WWW Basic Auth credentials
|
||||
"""
|
||||
|
||||
if optional and not req.get_param_as_bool("authenticate"):
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
if not req.auth:
|
||||
resp.append_header("WWW-Authenticate", "Basic")
|
||||
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate")
|
||||
|
||||
if not req.auth.startswith("Basic "):
|
||||
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
|
||||
|
||||
from base64 import b64decode
|
||||
basic, token = req.auth.split(" ", 1)
|
||||
user, passwd = b64decode(token).split(":", 1)
|
||||
|
||||
import simplepam
|
||||
if not simplepam.authenticate(user, passwd, "sshd"):
|
||||
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password")
|
||||
|
||||
req.context["user"] = User(user)
|
||||
req.context["groups"] = set()
|
||||
return account_info(func)(resource, req, resp, *args, **kwargs)
|
||||
|
||||
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
|
||||
return kerberos_authenticate
|
||||
elif config.AUTHENTICATION_BACKENDS == {"pam"}:
|
||||
return pam_authenticate
|
||||
elif config.AUTHENTICATION_BACKENDS == {"ldap"}:
|
||||
return ldap_authenticate
|
||||
else:
|
||||
raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS)
|
||||
return wrapper
|
||||
|
||||
|
||||
def login_required(func):
|
||||
return authenticate()(func)
|
||||
|
||||
|
||||
def login_optional(func):
|
||||
return authenticate(optional=True)(func)
|
||||
|
||||
|
||||
def authorize_admin(func):
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
from certidude import config
|
||||
# Parse remote IPv4/IPv6 address
|
||||
remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8"))
|
||||
|
||||
# Check for administration subnet whitelist
|
||||
print("Comparing:", config.ADMIN_SUBNETS, "To:", remote_addr)
|
||||
for subnet in config.ADMIN_SUBNETS:
|
||||
if subnet.overlaps(remote_addr):
|
||||
break
|
||||
else:
|
||||
logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr)
|
||||
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr)
|
||||
|
||||
def whitelist_authorize(resource, req, resp, *args, **kwargs):
|
||||
# Check for username whitelist
|
||||
if req.context.get("user") not in config.ADMIN_USERS:
|
||||
logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr)
|
||||
if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST:
|
||||
logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted",
|
||||
req.env["PATH_INFO"], req.context.get("user"), req.context.get("remote_addr"))
|
||||
raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user"))
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
# Retain username, TODO: Better abstraction with username, e-mail, sn, gn?
|
||||
if config.AUTHORIZATION_BACKEND == "whitelist":
|
||||
return whitelist_authorize
|
||||
else:
|
||||
return member_of(config.ADMINS_GROUP)(func)
|
||||
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
return wrapped
|
||||
|
@ -5,47 +5,64 @@ import re
|
||||
import socket
|
||||
import requests
|
||||
from OpenSSL import crypto
|
||||
from certidude import config, push
|
||||
from certidude import config, push, mailer
|
||||
from certidude.wrappers import Certificate, Request
|
||||
from certidude.signer import raw_sign
|
||||
from certidude import errors
|
||||
|
||||
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
|
||||
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
|
||||
|
||||
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
|
||||
# https://jamielinux.com/docs/openssl-certificate-authority/
|
||||
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
||||
|
||||
|
||||
# Cache CA certificate
|
||||
certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH))
|
||||
|
||||
def publish_certificate(func):
|
||||
# TODO: Implement e-mail and nginx notifications using hooks
|
||||
def wrapped(csr, *args, **kwargs):
|
||||
cert = func(csr, *args, **kwargs)
|
||||
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
||||
|
||||
if cert.email_address:
|
||||
mailer.send(
|
||||
"%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address),
|
||||
"certificate-signed.md",
|
||||
attachments=(cert,),
|
||||
certificate=cert)
|
||||
|
||||
if config.PUSH_PUBLISH:
|
||||
url = config.PUSH_PUBLISH % csr.fingerprint()
|
||||
click.echo("Publishing certificate at %s ..." % url)
|
||||
requests.post(url, data=cert.dump(),
|
||||
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
||||
|
||||
# For deleting request in the web view, use pubkey modulo
|
||||
push.publish("request-signed", csr.common_name)
|
||||
return cert
|
||||
return wrapped
|
||||
|
||||
|
||||
def get_request(common_name):
|
||||
if not re.match(RE_HOSTNAME, common_name):
|
||||
raise ValueError("Invalid common name")
|
||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||
return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem")))
|
||||
|
||||
|
||||
def get_signed(common_name):
|
||||
if not re.match(RE_HOSTNAME, common_name):
|
||||
raise ValueError("Invalid common name")
|
||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||
return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem")))
|
||||
|
||||
|
||||
def get_revoked(common_name):
|
||||
if not re.match(RE_HOSTNAME, common_name):
|
||||
raise ValueError("Invalid common name")
|
||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||
return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem")))
|
||||
|
||||
|
||||
def store_request(buf, overwrite=False):
|
||||
"""
|
||||
Store CSR for later processing
|
||||
@ -92,7 +109,7 @@ def revoke_certificate(common_name):
|
||||
cert = get_signed(common_name)
|
||||
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
|
||||
os.rename(cert.path, revoked_filename)
|
||||
push.publish("certificate-revoked", cert.fingerprint())
|
||||
push.publish("certificate-revoked", cert.common_name)
|
||||
|
||||
|
||||
def list_requests(directory=config.REQUESTS_DIR):
|
||||
@ -136,39 +153,50 @@ def delete_request(common_name):
|
||||
raise ValueError("Invalid common name")
|
||||
|
||||
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||
request_sha1sum = Request(open(path)).fingerprint()
|
||||
request = Request(open(path))
|
||||
os.unlink(path)
|
||||
|
||||
# Publish event at CA channel
|
||||
push.publish("request-deleted", request_sha1sum)
|
||||
push.publish("request-deleted", request.common_name)
|
||||
|
||||
# Write empty certificate to long-polling URL
|
||||
requests.delete(config.PUSH_PUBLISH % request_sha1sum,
|
||||
requests.delete(config.PUSH_PUBLISH % request.common_name,
|
||||
headers={"User-Agent": "Certidude API"})
|
||||
|
||||
def generate_p12_bundle(common_name):
|
||||
|
||||
def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
||||
"""
|
||||
Generate private key, sign certificate and return PKCS#12 bundle
|
||||
"""
|
||||
# Construct private key
|
||||
click.echo("Generating 4096-bit RSA key...")
|
||||
click.echo("Generating %d-bit RSA key..." % key_size)
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, 512)
|
||||
key.generate_key(crypto.TYPE_RSA, key_size)
|
||||
|
||||
# Construct CSR
|
||||
csr = crypto.X509Req()
|
||||
csr.set_version(2) # Corresponds to X.509v3
|
||||
csr.set_pubkey(key)
|
||||
csr.get_subject().CN = common_name
|
||||
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr).decode("utf-8")
|
||||
if owner:
|
||||
if owner.given_name:
|
||||
csr.get_subject().GN = owner.given_name
|
||||
if owner.surname:
|
||||
csr.get_subject().SN = owner.surname
|
||||
csr.add_extensions([
|
||||
crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)])
|
||||
|
||||
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
|
||||
|
||||
# Sign CSR
|
||||
cert = sign(Request(buf), overwrite=True)
|
||||
|
||||
# Generate P12
|
||||
ca_certs = crypto.load_certificate(crypto.FILETYPE_PEM, open(config.AUTHORITY_CERTIFICATE_PATH).read()),
|
||||
p12 = crypto.PKCS12()
|
||||
p12.set_privatekey( key )
|
||||
p12.set_certificate( cert._obj )
|
||||
p12.set_ca_certificates( ca_certs )
|
||||
return p12.export()
|
||||
p12.set_ca_certificates([certificate._obj])
|
||||
return p12.export(), cert
|
||||
|
||||
|
||||
@publish_certificate
|
||||
@ -187,7 +215,7 @@ def sign(req, overwrite=False, delete=True):
|
||||
elif req.pubkey == old_cert.pubkey:
|
||||
return old_cert
|
||||
else:
|
||||
raise FileExistsError("Will not overwrite existing certificate")
|
||||
raise EnvironmentError("Will not overwrite existing certificate")
|
||||
|
||||
# Sign via signer process
|
||||
cert_buf = signer_exec("sign-request", req.dump())
|
||||
@ -216,9 +244,9 @@ def sign2(request, overwrite=False, delete=True, lifetime=None):
|
||||
path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem")
|
||||
if os.path.exists(path):
|
||||
if overwrite:
|
||||
revoke(request.common_name)
|
||||
revoke_certificate(request.common_name)
|
||||
else:
|
||||
raise FileExistsError("File %s already exists!" % path)
|
||||
raise EnvironmentError("File %s already exists!" % path)
|
||||
|
||||
buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
with open(path + ".part", "wb") as fh:
|
||||
|
199
certidude/cli.py
@ -3,7 +3,6 @@
|
||||
|
||||
import asyncore
|
||||
import click
|
||||
import configparser
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
@ -14,11 +13,11 @@ import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
from certidude.signer import SignServer
|
||||
from certidude.common import expand_paths
|
||||
from configparser import ConfigParser
|
||||
from certidude import constants
|
||||
from certidude.common import expand_paths, ip_address, ip_network
|
||||
from datetime import datetime
|
||||
from humanize import naturaltime
|
||||
from ipaddress import ip_network, ip_address
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from time import sleep
|
||||
from setproctitle import setproctitle
|
||||
@ -66,7 +65,7 @@ if os.getuid() >= 1000:
|
||||
def certidude_request_spawn(fork):
|
||||
from certidude.helpers import certidude_request_certificate
|
||||
|
||||
clients = configparser.ConfigParser()
|
||||
clients = ConfigParser()
|
||||
clients.readfp(open("/etc/certidude/client.conf"))
|
||||
|
||||
services = ConfigParser()
|
||||
@ -92,7 +91,7 @@ def certidude_request_spawn(fork):
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
click.echo("Terminated process %d" % pid)
|
||||
os.unlink(pid_path)
|
||||
except (ValueError, ProcessLookupError, FileNotFoundError):
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
||||
if fork:
|
||||
@ -137,7 +136,7 @@ def certidude_request_spawn(fork):
|
||||
# Set up IPsec via NetworkManager
|
||||
if services.get(endpoint, "service") == "network-manager/strongswan":
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config = ConfigParser()
|
||||
config.add_section("connection")
|
||||
config.add_section("vpn")
|
||||
config.add_section("ipv4")
|
||||
@ -218,6 +217,7 @@ def certidude_signer_spawn(kill, no_interaction):
|
||||
"""
|
||||
Spawn privilege isolated signer process
|
||||
"""
|
||||
from certidude.signer import SignServer
|
||||
from certidude import config
|
||||
|
||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
||||
@ -254,7 +254,7 @@ def certidude_signer_spawn(kill, no_interaction):
|
||||
pid = int(fh.readline())
|
||||
os.kill(pid, 0)
|
||||
click.echo("Found process with PID %d" % pid)
|
||||
except (ValueError, ProcessLookupError, FileNotFoundError):
|
||||
except EnvironmentError:
|
||||
pid = 0
|
||||
|
||||
if pid > 0:
|
||||
@ -265,7 +265,7 @@ def certidude_signer_spawn(kill, no_interaction):
|
||||
sleep(1)
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
sleep(1)
|
||||
except ProcessLookupError:
|
||||
except EnvironmentError:
|
||||
pass
|
||||
|
||||
child_pid = os.fork()
|
||||
@ -280,15 +280,7 @@ def certidude_signer_spawn(kill, no_interaction):
|
||||
logging.basicConfig(
|
||||
filename="/var/log/signer.log",
|
||||
level=logging.INFO)
|
||||
server = SignServer(
|
||||
config.SIGNER_SOCKET_PATH,
|
||||
config.AUTHORITY_PRIVATE_KEY_PATH,
|
||||
config.AUTHORITY_CERTIFICATE_PATH,
|
||||
config.CERTIFICATE_LIFETIME,
|
||||
config.CERTIFICATE_BASIC_CONSTRAINTS,
|
||||
config.CERTIFICATE_KEY_USAGE_FLAGS,
|
||||
config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
|
||||
config.REVOCATION_LIST_LIFETIME)
|
||||
server = SignServer()
|
||||
asyncore.loop()
|
||||
|
||||
|
||||
@ -363,8 +355,8 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
|
||||
common_name,
|
||||
org_unit,
|
||||
email_address,
|
||||
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
|
||||
extended_key_usage="serverAuth,ikeIntermediate",
|
||||
key_usage="digitalSignature,keyEncipherment",
|
||||
extended_key_usage="serverAuth",
|
||||
wait=True)
|
||||
|
||||
if not os.path.exists(dhparam_path):
|
||||
@ -375,7 +367,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
|
||||
return retval
|
||||
|
||||
# TODO: Add dhparam
|
||||
config.write(env.get_template("openvpn-site-to-client.ovpn").render(locals()))
|
||||
config.write(env.get_template("openvpn-site-to-client.ovpn").render(vars()))
|
||||
|
||||
click.echo("Generated %s" % config.name)
|
||||
click.echo()
|
||||
@ -385,6 +377,74 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
|
||||
click.echo()
|
||||
|
||||
|
||||
@click.command("nginx", help="Set up nginx as HTTPS server")
|
||||
@click.argument("url")
|
||||
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
|
||||
@click.option("--org-unit", "-ou", help="Organizational unit")
|
||||
@click.option("--tls-config",
|
||||
default="/etc/nginx/conf.d/tls.conf",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
|
||||
@click.option("--site-config", "-o",
|
||||
default="/etc/nginx/sites-available/%s.conf" % HOSTNAME,
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % HOSTNAME)
|
||||
@click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default")
|
||||
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
|
||||
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
|
||||
@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
|
||||
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
|
||||
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
|
||||
@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
|
||||
@expand_paths()
|
||||
def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client):
|
||||
# TODO: Intelligent way of getting last IP address in the subnet
|
||||
from certidude.helpers import certidude_request_certificate
|
||||
|
||||
if not os.path.exists(certificate_path):
|
||||
click.echo("As HTTPS server certificate needs specific key usage extensions please")
|
||||
click.echo("use following command to sign on Certidude server instead of web interface:")
|
||||
click.echo()
|
||||
click.echo(" certidude sign %s" % common_name)
|
||||
click.echo()
|
||||
retval = certidude_request_certificate(url, key_path, request_path,
|
||||
certificate_path, authority_path, common_name, org_unit,
|
||||
key_usage="digitalSignature,keyEncipherment",
|
||||
extended_key_usage="serverAuth",
|
||||
dns = constants.FQDN, wait=True, bundle=True)
|
||||
|
||||
if not os.path.exists(dhparam_path):
|
||||
cmd = "openssl", "dhparam", "-out", dhparam_path, "2048"
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
if retval:
|
||||
return retval
|
||||
|
||||
context = globals() # Grab constants.BLAH
|
||||
context.update(locals())
|
||||
|
||||
if os.path.exists(site_config.name):
|
||||
click.echo("Configuration file %s already exists, not overwriting" % site_config.name)
|
||||
else:
|
||||
site_config.write(env.get_template("nginx-https-site.conf").render(context))
|
||||
click.echo("Generated %s" % site_config.name)
|
||||
|
||||
if os.path.exists(tls_config.name):
|
||||
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
|
||||
else:
|
||||
tls_config.write(env.get_template("nginx-tls.conf").render(context))
|
||||
click.echo("Generated %s" % tls_config.name)
|
||||
|
||||
click.echo()
|
||||
click.echo("Inspect configuration files, enable it and start nginx service:")
|
||||
click.echo()
|
||||
click.echo(" ln -s %s /etc/nginx/sites-enabled/%s" % (
|
||||
os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"),
|
||||
os.path.basename(site_config.name)))
|
||||
click.secho(" service nginx restart", bold=True)
|
||||
click.echo()
|
||||
|
||||
|
||||
@click.command("client", help="Set up OpenVPN client")
|
||||
@click.argument("url")
|
||||
@click.argument("remote")
|
||||
@ -419,7 +479,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
||||
return retval
|
||||
|
||||
# TODO: Add dhparam
|
||||
config.write(env.get_template("openvpn-client-to-site.ovpn").render(locals()))
|
||||
config.write(env.get_template("openvpn-client-to-site.ovpn").render(vars()))
|
||||
|
||||
click.echo("Generated %s" % config.name)
|
||||
click.echo()
|
||||
@ -435,8 +495,8 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
|
||||
@click.option("--org-unit", "-ou", help="Organizational unit")
|
||||
@click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate")
|
||||
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL)
|
||||
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
|
||||
@click.option("--local", "-l", default=None, type=ip_address, help="IP address associated with the certificate, none by default")
|
||||
@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
|
||||
@click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default")
|
||||
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
||||
@click.option("--config", "-o",
|
||||
default="/etc/ipsec.conf",
|
||||
@ -473,7 +533,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
|
||||
common_name,
|
||||
org_unit,
|
||||
email_address,
|
||||
key_usage="nonRepudiation,digitalSignature,keyEncipherment",
|
||||
key_usage="digitalSignature,keyEncipherment",
|
||||
extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2",
|
||||
ip_address=local,
|
||||
dns=fqdn,
|
||||
@ -482,7 +542,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
|
||||
if retval:
|
||||
return retval
|
||||
|
||||
config.write(env.get_template("strongswan-site-to-client.conf").render(locals()))
|
||||
config.write(env.get_template("strongswan-site-to-client.conf").render(vars()))
|
||||
secrets.write(": RSA %s\n" % key_path)
|
||||
|
||||
click.echo("Generated %s and %s" % (config.name, secrets.name))
|
||||
@ -539,7 +599,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
|
||||
return retval
|
||||
|
||||
# TODO: Add dhparam
|
||||
config.write(env.get_template("strongswan-client-to-site.conf").render(locals()))
|
||||
config.write(env.get_template("strongswan-client-to-site.conf").render(vars()))
|
||||
secrets.write(": RSA %s\n" % key_path)
|
||||
|
||||
click.echo("Generated %s and %s" % (config.name, secrets.name))
|
||||
@ -584,7 +644,7 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o
|
||||
csum = csummer.hexdigest()
|
||||
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config = ConfigParser()
|
||||
config.add_section("connection")
|
||||
config.add_section("vpn")
|
||||
config.add_section("ipv4")
|
||||
@ -620,11 +680,12 @@ def certidude_setup_strongswan_networkmanager(url, email_address, common_name, o
|
||||
subprocess.call(("nmcli", "c", "up", "uuid", uuid))
|
||||
|
||||
|
||||
@click.command("production", help="Set up nginx and uwsgi")
|
||||
@click.command("production", help="Set up nginx, uwsgi and cron")
|
||||
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
||||
@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME)
|
||||
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files")
|
||||
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab")
|
||||
@click.option("--push-server", default=None, help="Push server URL")
|
||||
@click.option("--nginx-config", "-n",
|
||||
default="/etc/nginx/nginx.conf",
|
||||
type=click.File(mode="w", atomic=True, lazy=True),
|
||||
@ -642,19 +703,36 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
if subprocess.call("net ads testjoin", shell=True):
|
||||
click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", stderr=True)
|
||||
click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", err=True)
|
||||
exit(255)
|
||||
|
||||
if not os.path.exists(kerberos_keytab):
|
||||
subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P")
|
||||
click.echo("Created Kerberos keytab in '%s'" % kerberos_keytab)
|
||||
click.echo("Created service principal in Kerberos keytab '%s'" % kerberos_keytab)
|
||||
|
||||
if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"):
|
||||
# Fetch Kerberos ticket for system account
|
||||
cp = ConfigParser()
|
||||
cp.read("/etc/samba/smb.conf")
|
||||
domain = cp.get("global", "realm").lower()
|
||||
base = ",".join(["dc=" + j for j in domain.split(".")])
|
||||
with open("/etc/cron.hourly/certidude", "w") as fh:
|
||||
fh.write("#!/bin/bash\n")
|
||||
fh.write("KRB5CCNAME=/run/certidude/krb5cc-new kinit -k %s$\n" % cp.get("global", "netbios name"))
|
||||
fh.write("chown certidude /run/certidude/krb5cc-new\n")
|
||||
fh.write("mv /run/certidude/krb5cc-new /run/certidude/krb5cc\n")
|
||||
os.chmod("/etc/cron.hourly/certidude", 0o755)
|
||||
click.echo("Created /etc/cron.hourly/certidude for automatic Kerberos TGT renewal")
|
||||
else:
|
||||
click.echo("Warning: cronjob for Kerberos ticket renewal not created, LDAP with GSSAPI will not be available!")
|
||||
|
||||
|
||||
if not static_path.endswith("/"):
|
||||
static_path += "/"
|
||||
|
||||
nginx_config.write(env.get_template("nginx.conf").render(locals()))
|
||||
nginx_config.write(env.get_template("nginx.conf").render(vars()))
|
||||
click.echo("Generated: %s" % nginx_config.name)
|
||||
uwsgi_config.write(env.get_template("uwsgi.ini").render(locals()))
|
||||
uwsgi_config.write(env.get_template("uwsgi.ini").render(vars()))
|
||||
click.echo("Generated: %s" % uwsgi_config.name)
|
||||
|
||||
if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"):
|
||||
@ -663,7 +741,7 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
|
||||
click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name)
|
||||
|
||||
if not push_server:
|
||||
click.echo("Remember to install nginx with wandenberg/nginx-push-stream-module!")
|
||||
click.echo("Remember to install nchan instead of regular nginx!")
|
||||
|
||||
|
||||
@click.command("authority", help="Set up Certificate Authority in a directory")
|
||||
@ -735,6 +813,9 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60)
|
||||
ca.set_issuer(ca.get_subject())
|
||||
ca.set_pubkey(key)
|
||||
|
||||
# add_extensions shall be called only once and
|
||||
# there has to be only one subjectAltName!
|
||||
ca.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints",
|
||||
@ -746,7 +827,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
b"keyCertSign, cRLSign"),
|
||||
crypto.X509Extension(
|
||||
b"extendedKeyUsage",
|
||||
True,
|
||||
False,
|
||||
b"serverAuth,1.3.6.1.5.5.8.2.2"),
|
||||
crypto.X509Extension(
|
||||
b"subjectKeyIdentifier",
|
||||
@ -756,21 +837,11 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
crypto.X509Extension(
|
||||
b"crlDistributionPoints",
|
||||
False,
|
||||
crl_distribution_points.encode("ascii"))
|
||||
])
|
||||
|
||||
subject_alt_name = "email:%s" % email_address
|
||||
ca.add_extensions([
|
||||
crl_distribution_points.encode("ascii")),
|
||||
crypto.X509Extension(
|
||||
b"subjectAltName",
|
||||
False,
|
||||
subject_alt_name.encode("ascii"))
|
||||
])
|
||||
ca.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"subjectAltName",
|
||||
True,
|
||||
("DNS:%s" % common_name).encode("ascii"))
|
||||
"DNS: %s, email: %s" % (common_name.encode("ascii"), email_address.encode("ascii")))
|
||||
])
|
||||
|
||||
if ocsp_responder_url:
|
||||
@ -819,7 +890,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or
|
||||
# Set permission bits to 640
|
||||
os.umask(0o137)
|
||||
with open(certidude_conf, "w") as fh:
|
||||
fh.write(env.get_template("certidude.conf").render(locals()))
|
||||
fh.write(env.get_template("certidude.conf").render(vars()))
|
||||
with open(ca_crt, "wb") as fh:
|
||||
fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca))
|
||||
|
||||
@ -988,12 +1059,23 @@ def certidude_sign(common_name, overwrite, lifetime):
|
||||
click.echo("Added extension %s: %s" % (key, value))
|
||||
click.echo()
|
||||
|
||||
|
||||
@click.command("serve", help="Run built-in HTTP server")
|
||||
@click.option("-u", "--user", default="certidude", help="Run as user")
|
||||
@click.option("-p", "--port", default=80, help="Listen port")
|
||||
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
||||
@click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA")
|
||||
def certidude_serve(user, port, listen, enable_signature):
|
||||
from certidude import config
|
||||
|
||||
click.echo("Users subnets: %s" %
|
||||
", ".join([str(j) for j in config.USER_SUBNETS]))
|
||||
click.echo("Administrative subnets: %s" %
|
||||
", ".join([str(j) for j in config.ADMIN_SUBNETS]))
|
||||
click.echo("Auto-sign enabled for following subnets: %s" %
|
||||
", ".join([str(j) for j in config.AUTOSIGN_SUBNETS]))
|
||||
click.echo("Request submissions allowed from following subnets: %s" %
|
||||
", ".join([str(j) for j in config.REQUEST_SUBNETS]))
|
||||
|
||||
logging.basicConfig(
|
||||
filename='/var/log/certidude.log',
|
||||
@ -1004,13 +1086,15 @@ def certidude_serve(user, port, listen, enable_signature):
|
||||
from wsgiref.simple_server import make_server, WSGIServer
|
||||
from socketserver import ThreadingMixIn
|
||||
from certidude.api import certidude_app, StaticResource
|
||||
from certidude import config
|
||||
|
||||
class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
|
||||
pass
|
||||
|
||||
click.echo("Listening on %s:%d" % (listen, port))
|
||||
|
||||
|
||||
# TODO: Bind before dropping privileges,
|
||||
# but create app (sqlite log files!) after dropping privileges
|
||||
app = certidude_app()
|
||||
|
||||
app.add_sink(StaticResource(os.path.join(os.path.dirname(__file__), "static")))
|
||||
@ -1023,25 +1107,25 @@ def certidude_serve(user, port, listen, enable_signature):
|
||||
from jinja2.debug import make_traceback as _make_traceback
|
||||
"".encode("charmap")
|
||||
|
||||
if config.AUTHENTICATION_BACKEND == "pam":
|
||||
restricted_groups = []
|
||||
|
||||
if config.AUTHENTICATION_BACKENDS == {"pam"}:
|
||||
# PAM needs access to /etc/shadow
|
||||
import grp
|
||||
name, passwd, gid, mem = grp.getgrnam("shadow")
|
||||
click.echo("Adding current user to shadow group due to PAM authentication backend")
|
||||
os.setgroups([gid])
|
||||
else:
|
||||
os.setgroups([])
|
||||
restricted_groups.append(gid)
|
||||
|
||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(user)
|
||||
if uid == 0:
|
||||
click.echo("Please specify unprivileged user")
|
||||
exit(254)
|
||||
restricted_groups.append(gid)
|
||||
|
||||
|
||||
os.setgroups(restricted_groups)
|
||||
os.setgid(gid)
|
||||
os.setuid(uid)
|
||||
|
||||
click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" %
|
||||
(user, uid, gid, ", ".join([str(j) for j in os.getgroups()])))
|
||||
(user, os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()])))
|
||||
|
||||
os.umask(0o007)
|
||||
elif os.getuid() == 0:
|
||||
@ -1076,6 +1160,7 @@ certidude_setup.add_command(certidude_setup_openvpn)
|
||||
certidude_setup.add_command(certidude_setup_strongswan)
|
||||
certidude_setup.add_command(certidude_setup_client)
|
||||
certidude_setup.add_command(certidude_setup_production)
|
||||
certidude_setup.add_command(certidude_setup_nginx)
|
||||
certidude_request.add_command(certidude_request_spawn)
|
||||
certidude_signer.add_command(certidude_signer_spawn)
|
||||
entry_point.add_command(certidude_setup)
|
||||
|
@ -1,6 +1,13 @@
|
||||
|
||||
import os
|
||||
import click
|
||||
import ipaddress
|
||||
|
||||
def ip_network(j):
|
||||
return ipaddress.ip_network(unicode(j))
|
||||
|
||||
def ip_address(j):
|
||||
return ipaddress.ip_address(unicode(j))
|
||||
|
||||
def expand_paths():
|
||||
"""
|
||||
|
@ -4,50 +4,52 @@ import codecs
|
||||
import configparser
|
||||
import ipaddress
|
||||
import os
|
||||
import socket
|
||||
import string
|
||||
from random import choice
|
||||
from urllib.parse import urlparse
|
||||
|
||||
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
|
||||
|
||||
cp = configparser.ConfigParser()
|
||||
cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8"))
|
||||
|
||||
AUTHENTICATION_BACKEND = cp.get("authentication", "backend") # kerberos, pam
|
||||
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, pam
|
||||
AUTHENTICATION_BACKENDS = set([j for j in
|
||||
cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap
|
||||
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix
|
||||
ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
|
||||
|
||||
ADMIN_USERS = set([j for j in cp.get("authorization", "admin_users").split(" ") if j])
|
||||
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j])
|
||||
AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "autosign_subnets").split(" ") if j])
|
||||
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "request_subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
|
||||
USER_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "user subnets").split(" ") if j])
|
||||
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "admin subnets").split(" ") if j]).union(USER_SUBNETS)
|
||||
AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "autosign subnets").split(" ") if j])
|
||||
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
|
||||
|
||||
SIGNER_SOCKET_PATH = "/run/certidude/signer.sock"
|
||||
SIGNER_PID_PATH = "/run/certidude/signer.pid"
|
||||
|
||||
AUTHORITY_DIR = "/var/lib/certidude"
|
||||
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private_key_path")
|
||||
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate_path")
|
||||
REQUESTS_DIR = cp.get("authority", "requests_dir")
|
||||
SIGNED_DIR = cp.get("authority", "signed_dir")
|
||||
REVOKED_DIR = cp.get("authority", "revoked_dir")
|
||||
|
||||
#LOG_DATA = cp.get("logging", "database")
|
||||
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
||||
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
|
||||
REQUESTS_DIR = cp.get("authority", "requests dir")
|
||||
SIGNED_DIR = cp.get("authority", "signed dir")
|
||||
REVOKED_DIR = cp.get("authority", "revoked dir")
|
||||
OUTBOX = cp.get("authority", "outbox")
|
||||
|
||||
CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE"
|
||||
CERTIFICATE_KEY_USAGE_FLAGS = "nonRepudiation,digitalSignature,keyEncipherment"
|
||||
CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment"
|
||||
CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth"
|
||||
CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate_lifetime"))
|
||||
CERTIFICATE_LIFETIME = int(cp.get("signature", "certificate lifetime"))
|
||||
|
||||
REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation_list_lifetime"))
|
||||
REVOCATION_LIST_LIFETIME = int(cp.get("signature", "revocation list lifetime"))
|
||||
|
||||
PUSH_TOKEN = "".join([choice(string.ascii_letters + string.digits) for j in range(0,32)])
|
||||
|
||||
PUSH_TOKEN = "ca"
|
||||
|
||||
try:
|
||||
PUSH_EVENT_SOURCE = cp.get("push", "event_source")
|
||||
PUSH_LONG_POLL = cp.get("push", "long_poll")
|
||||
PUSH_EVENT_SOURCE = cp.get("push", "event source")
|
||||
PUSH_LONG_POLL = cp.get("push", "long poll")
|
||||
PUSH_PUBLISH = cp.get("push", "publish")
|
||||
except configparser.NoOptionError:
|
||||
PUSH_SERVER = cp.get("push", "server") or "http://localhost"
|
||||
@ -55,18 +57,41 @@ except configparser.NoOptionError:
|
||||
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s"
|
||||
PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s"
|
||||
|
||||
o = urlparse(cp.get("authority", "database") if cp.has_option("authority", "database") else "")
|
||||
|
||||
if not o.scheme:
|
||||
DATABASE_POOL = None
|
||||
elif o.scheme == "mysql":
|
||||
import mysql.connector
|
||||
DATABASE_POOL = mysql.connector.pooling.MySQLConnectionPool(
|
||||
pool_size = 32,
|
||||
user=o.username,
|
||||
password=o.password,
|
||||
host=o.hostname,
|
||||
database=o.path[1:])
|
||||
TAGGING_BACKEND = cp.get("tagging", "backend")
|
||||
LOGGING_BACKEND = cp.get("logging", "backend")
|
||||
LEASES_BACKEND = cp.get("leases", "backend")
|
||||
|
||||
|
||||
if "whitelist" == AUTHORIZATION_BACKEND:
|
||||
USERS_WHITELIST = set([j for j in cp.get("authorization", "users whitelist").split(" ") if j])
|
||||
ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j])
|
||||
elif "posix" == AUTHORIZATION_BACKEND:
|
||||
USERS_GROUP = cp.get("authorization", "posix user group")
|
||||
ADMINS_GROUP = cp.get("authorization", "posix admin group")
|
||||
elif "ldap" == AUTHORIZATION_BACKEND:
|
||||
USERS_GROUP = cp.get("authorization", "ldap user group")
|
||||
ADMINS_GROUP = cp.get("authorization", "ldap admin group")
|
||||
else:
|
||||
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database is supported" % o.scheme)
|
||||
raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND)
|
||||
|
||||
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
|
||||
LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter")
|
||||
LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter")
|
||||
LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter")
|
||||
|
||||
for line in open("/etc/ldap/ldap.conf"):
|
||||
line = line.strip().lower()
|
||||
if "#" in line:
|
||||
line, _ = line.split("#", 1)
|
||||
if not " " in line:
|
||||
continue
|
||||
key, value = line.split(" ", 1)
|
||||
if key == "uri":
|
||||
LDAP_SERVERS = set([j for j in value.split(" ") if j])
|
||||
click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS))
|
||||
elif key == "base":
|
||||
LDAP_BASE = value
|
||||
else:
|
||||
click.echo("No LDAP servers specified in /etc/ldap/ldap.conf")
|
||||
|
||||
|
12
certidude/constants.py
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
import socket
|
||||
|
||||
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
|
||||
|
||||
if "." in FQDN:
|
||||
HOSTNAME, DOMAIN = FQDN.split(".", 1)
|
||||
else:
|
||||
HOSTNAME, DOMAIN = FQDN, "local"
|
||||
click.echo("Unable to determine domain of this computer, falling back to local")
|
||||
|
||||
EXTENSION_WHITELIST = set(["subjectAltName"])
|
@ -1,13 +1,39 @@
|
||||
# encoding: utf-8
|
||||
|
||||
import falcon
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import types
|
||||
from datetime import date, time, datetime
|
||||
from OpenSSL import crypto
|
||||
from certidude.wrappers import Request, Certificate
|
||||
from urllib.parse import urlparse
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
def csrf_protection(func):
|
||||
"""
|
||||
Protect resource from common CSRF attacks by checking user agent and referrer
|
||||
"""
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
# Assume curl and python-requests are used intentionally
|
||||
if req.user_agent.startswith("curl/") or req.user_agent.startswith("python-requests/"):
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
|
||||
# For everything else assert referrer
|
||||
referrer = req.headers.get("REFERER")
|
||||
if referrer:
|
||||
scheme, netloc, path, params, query, fragment = urlparse(referrer)
|
||||
if netloc == req.host:
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
|
||||
# Kaboom!
|
||||
logger.warning("Prevented clickbait from '%s' with user agent '%s'",
|
||||
referrer or "-", req.user_agent)
|
||||
raise falcon.HTTPUnauthorized("Forbidden",
|
||||
"No suitable UA or referrer provided, cross-site scripting disabled")
|
||||
return wrapped
|
||||
|
||||
|
||||
def event_source(func):
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
@ -15,7 +41,6 @@ def event_source(func):
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid
|
||||
resp.body = "Redirecting to:" + resp.location
|
||||
print("Delegating EventSource handling to:", resp.location)
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
@ -24,9 +49,10 @@ class MyEncoder(json.JSONEncoder):
|
||||
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
|
||||
"key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage"
|
||||
|
||||
CERTIFICATE_ATTRIBUTES = "revokable", "identity", "changed", "common_name", \
|
||||
CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \
|
||||
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
|
||||
"key_type", "key_length", "sha256sum", "serial_number", "key_usage"
|
||||
"key_type", "key_length", "sha256sum", "serial_number", "key_usage", \
|
||||
"signed", "expires"
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, crypto.X509Name):
|
||||
@ -60,18 +86,25 @@ def serialize(func):
|
||||
Falcon response serialization
|
||||
"""
|
||||
def wrapped(instance, req, resp, **kwargs):
|
||||
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
||||
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
resp.set_header("Pragma", "no-cache");
|
||||
resp.set_header("Expires", "0");
|
||||
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
resp.set_header("Pragma", "no-cache")
|
||||
resp.set_header("Expires", "0")
|
||||
r = func(instance, req, resp, **kwargs)
|
||||
if resp.body is None:
|
||||
if req.get_header("Accept").split(",")[0] == "application/json":
|
||||
if req.accept.startswith("application/json"):
|
||||
resp.set_header("Content-Type", "application/json")
|
||||
resp.set_header("Content-Disposition", "inline")
|
||||
resp.body = json.dumps(r, cls=MyEncoder)
|
||||
|
||||
elif hasattr(r, "content_type") and req.client_accepts(r.content_type):
|
||||
resp.set_header("Content-Type", r.content_type)
|
||||
resp.set_header("Content-Disposition",
|
||||
("attachment; filename=%s" % r.suggested_filename).encode("ascii"))
|
||||
resp.body = r.dump()
|
||||
else:
|
||||
resp.body = repr(r)
|
||||
logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept))
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"Client did not accept application/json or %s" % r.content_type)
|
||||
return r
|
||||
return wrapped
|
||||
|
||||
|
38
certidude/firewall.py
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
import falcon
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
def whitelist_subnets(subnets):
|
||||
"""
|
||||
Validate source IP address of API call against subnet list
|
||||
"""
|
||||
def wrapper(func):
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
# Check for administration subnet whitelist
|
||||
for subnet in subnets:
|
||||
if req.context.get("remote_addr") in subnet:
|
||||
break
|
||||
else:
|
||||
logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted",
|
||||
req.env["PATH_INFO"],
|
||||
req.context.get("user", "unauthenticated user"),
|
||||
req.context.get("remote_addr"))
|
||||
raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr)
|
||||
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
def whitelist_content_types(*content_types):
|
||||
def wrapper(func):
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
for content_type in content_types:
|
||||
if req.get_header("Content-Type") == content_type:
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"This API call accepts only %s content type" % ", ".join(content_types))
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
@ -6,7 +6,7 @@ from certidude import errors
|
||||
from certidude.wrappers import Certificate, Request
|
||||
from OpenSSL import crypto
|
||||
|
||||
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None):
|
||||
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False):
|
||||
"""
|
||||
Exchange CSR for certificate using Certidude HTTP API server
|
||||
"""
|
||||
@ -41,7 +41,8 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
click.echo("Attempting to fetch CA certificate from %s" % authority_url)
|
||||
|
||||
try:
|
||||
r = requests.get(authority_url)
|
||||
r = requests.get(authority_url,
|
||||
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % r.text)
|
||||
@ -53,7 +54,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
try:
|
||||
request = Request(open(request_path))
|
||||
click.echo("Found signing request: %s" % request_path)
|
||||
except FileNotFoundError:
|
||||
except EnvironmentError:
|
||||
|
||||
# Construct private key
|
||||
click.echo("Generating 4096-bit RSA key...")
|
||||
@ -69,10 +70,11 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
csr = crypto.X509Req()
|
||||
csr.set_version(2) # Corresponds to X.509v3
|
||||
csr.set_pubkey(key)
|
||||
csr.get_subject().CN = common_name
|
||||
|
||||
request = Request(csr)
|
||||
|
||||
# Set subject attributes
|
||||
request.common_name = common_name
|
||||
if given_name:
|
||||
request.given_name = given_name
|
||||
if surname:
|
||||
@ -83,20 +85,20 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
# Collect subject alternative names
|
||||
subject_alt_name = set()
|
||||
if email_address:
|
||||
subject_alt_name.add("email:" + email_address)
|
||||
subject_alt_name.add("email:%s" % email_address)
|
||||
if ip_address:
|
||||
subject_alt_name.add("IP:" + ip_address)
|
||||
subject_alt_name.add("IP:%s" % ip_address)
|
||||
if dns:
|
||||
subject_alt_name.add("DNS:" + dns)
|
||||
subject_alt_name.add("DNS:%s" % dns)
|
||||
|
||||
# Set extensions
|
||||
extensions = []
|
||||
if key_usage:
|
||||
extensions.append(("keyUsage", key_usage, True))
|
||||
if extended_key_usage:
|
||||
extensions.append(("extendedKeyUsage", extended_key_usage, True))
|
||||
extensions.append(("extendedKeyUsage", extended_key_usage, False))
|
||||
if subject_alt_name:
|
||||
extensions.append(("subjectAltName", ", ".join(subject_alt_name), True))
|
||||
extensions.append(("subjectAltName", ", ".join(subject_alt_name), False))
|
||||
request.set_extensions(extensions)
|
||||
|
||||
# Dump CSR
|
||||
@ -113,7 +115,7 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
click.echo("Submitting to %s, waiting for response..." % request_url)
|
||||
submission = requests.post(request_url,
|
||||
data=open(request_path),
|
||||
headers={"User-Agent": "Certidude", "Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert"})
|
||||
headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"})
|
||||
|
||||
if submission.status_code == requests.codes.ok:
|
||||
pass
|
||||
@ -131,12 +133,18 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
|
||||
try:
|
||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text)
|
||||
except crypto.Error:
|
||||
raise ValueError("Failed to parse PEM: %s" % buf)
|
||||
raise ValueError("Failed to parse PEM: %s" % submission.text)
|
||||
|
||||
os.umask(0o022)
|
||||
with open(certificate_path + ".part", "w") as fh:
|
||||
# Dump certificate
|
||||
fh.write(submission.text)
|
||||
|
||||
# Bundle CA certificate, necessary for nginx
|
||||
if bundle:
|
||||
with open(authority_path) as ch:
|
||||
fh.write(ch.read())
|
||||
|
||||
click.echo("Writing certificate to: %s" % certificate_path)
|
||||
os.rename(certificate_path + ".part", certificate_path)
|
||||
|
||||
|
@ -1,104 +1,90 @@
|
||||
|
||||
import os
|
||||
import smtplib
|
||||
from time import sleep
|
||||
from markdown import markdown
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.base import MIMEBase
|
||||
from urllib.parse import urlparse
|
||||
|
||||
class Mailer(object):
|
||||
def __init__(self, url):
|
||||
scheme, netloc, path, params, query, fragment = urlparse(url)
|
||||
scheme = scheme.lower()
|
||||
env = Environment(loader=PackageLoader("certidude", "templates/mail"))
|
||||
|
||||
if path:
|
||||
raise ValueError("Path for URL not supported")
|
||||
if params:
|
||||
raise ValueError("Parameters for URL not supported")
|
||||
if query:
|
||||
raise ValueError("Query for URL not supported")
|
||||
if fragment:
|
||||
raise ValueError("Fragment for URL not supported")
|
||||
def send(recipients, template, attachments=(), **context):
|
||||
from certidude import authority, config
|
||||
if not config.OUTBOX:
|
||||
# Mailbox disabled, don't send e-mail
|
||||
return
|
||||
|
||||
if not recipients:
|
||||
raise ValueError("No e-mail recipients specified!")
|
||||
|
||||
scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX)
|
||||
scheme = scheme.lower()
|
||||
|
||||
if path:
|
||||
raise ValueError("Path for URL not supported")
|
||||
if params:
|
||||
raise ValueError("Parameters for URL not supported")
|
||||
if query:
|
||||
raise ValueError("Query for URL not supported")
|
||||
if fragment:
|
||||
raise ValueError("Fragment for URL not supported")
|
||||
|
||||
|
||||
self.username = None
|
||||
self.password = ""
|
||||
username = None
|
||||
password = ""
|
||||
|
||||
if scheme == "smtp":
|
||||
self.secure = False
|
||||
self.port = 25
|
||||
elif scheme == "smtps":
|
||||
self.secure = True
|
||||
self.port = 465
|
||||
if scheme == "smtp":
|
||||
secure = False
|
||||
port = 25
|
||||
elif scheme == "smtps":
|
||||
secure = True
|
||||
port = 465
|
||||
else:
|
||||
raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme)
|
||||
|
||||
if "@" in netloc:
|
||||
credentials, netloc = netloc.split("@")
|
||||
|
||||
if ":" in credentials:
|
||||
username, password = credentials.split(":")
|
||||
else:
|
||||
raise ValueError("Unknown scheme '%s', currently SMTP and SMTPS are only supported" % scheme)
|
||||
username = credentials
|
||||
|
||||
if "@" in netloc:
|
||||
credentials, netloc = netloc.split("@")
|
||||
|
||||
if ":" in credentials:
|
||||
self.username, self.password = credentials.split(":")
|
||||
else:
|
||||
self.username = credentials
|
||||
|
||||
if ":" in netloc:
|
||||
self.server, port_str = netloc.split(":")
|
||||
self.port = int(port_str)
|
||||
else:
|
||||
self.server = netloc
|
||||
|
||||
self.env = Environment(loader=PackageLoader("certidude", "email_templates"))
|
||||
self.conn = None
|
||||
|
||||
def reconnect(self):
|
||||
# Gmail employs some sort of IPS
|
||||
# https://accounts.google.com/DisplayUnlockCaptcha
|
||||
print("Connecting to:", self.server, self.port)
|
||||
self.conn = smtplib.SMTP(self.server, self.port)
|
||||
if self.secure:
|
||||
self.conn.starttls()
|
||||
if self.username and self.password:
|
||||
self.conn.login(self.username, self.password)
|
||||
|
||||
def enqueue(self, sender, recipients, subject, template, **context):
|
||||
self.send(sender, recipients, subject, template, **context)
|
||||
if ":" in netloc:
|
||||
server, port_str = netloc.split(":")
|
||||
port = int(port_str)
|
||||
else:
|
||||
server = netloc
|
||||
|
||||
|
||||
def send(self, sender, recipients, subject, template, **context):
|
||||
subject, text = env.get_template(template).render(context).split("\n\n", 1)
|
||||
html = markdown(text)
|
||||
|
||||
recipients = [j for j in recipients if j]
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = authority.certificate.email_address
|
||||
msg["To"] = recipients
|
||||
|
||||
if not recipients:
|
||||
print("No recipients to send e-mail to!")
|
||||
return
|
||||
print("Sending e-mail to:", recipients, "body follows:")
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = sender
|
||||
msg["To"] = ", ".join(recipients)
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
|
||||
text = self.env.get_template(template + ".txt").render(context)
|
||||
html = self.env.get_template(template + ".html").render(context)
|
||||
for attachment in attachments:
|
||||
part = MIMEBase(*attachment.content_type.split("/"))
|
||||
part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename)
|
||||
part.set_payload(attachment.dump())
|
||||
msg.attach(part)
|
||||
|
||||
print(text)
|
||||
# Gmail employs some sort of IPS
|
||||
# https://accounts.google.com/DisplayUnlockCaptcha
|
||||
conn = smtplib.SMTP(server, port)
|
||||
if secure:
|
||||
conn.starttls()
|
||||
if username and password:
|
||||
conn.login(username, password)
|
||||
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
|
||||
backoff = 1
|
||||
while True:
|
||||
try:
|
||||
if not self.conn:
|
||||
self.reconnect()
|
||||
self.conn.sendmail(sender, recipients, msg.as_string())
|
||||
return
|
||||
except smtplib.SMTPServerDisconnected:
|
||||
print("Connection to %s unexpectedly closed, probably TCP timeout, backing off for %d second" % (self.server, backoff))
|
||||
self.reconnect()
|
||||
backoff = backoff * 2
|
||||
sleep(backoff)
|
||||
conn.sendmail(authority.certificate.email_address, recipients, msg.as_string())
|
||||
|
@ -1,34 +1,17 @@
|
||||
|
||||
import logging
|
||||
import time
|
||||
from certidude.api.tag import RelationalMixin
|
||||
|
||||
class MySQLLogHandler(logging.Handler):
|
||||
class LogHandler(logging.Handler, RelationalMixin):
|
||||
SQL_CREATE_TABLES = "log_tables.sql"
|
||||
|
||||
SQL_CREATE_TABLE = """CREATE TABLE IF NOT EXISTS log(
|
||||
created datetime, facility varchar(30), level int,
|
||||
severity varchar(10), message text, module varchar(20),
|
||||
func varchar(20), lineno int, exception text, process int,
|
||||
thread text, thread_name text)"""
|
||||
|
||||
SQL_INSERT_ENTRY = """insert into log( created, facility, level, severity,
|
||||
message, module, func, lineno, exception, process, thread,
|
||||
thread_name) values (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);
|
||||
"""
|
||||
|
||||
def __init__(self, pool):
|
||||
def __init__(self, uri):
|
||||
logging.Handler.__init__(self)
|
||||
self.pool = pool
|
||||
conn = self.pool.get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(self.SQL_CREATE_TABLE)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
RelationalMixin.__init__(self, uri)
|
||||
|
||||
def emit(self, record):
|
||||
conn = self.pool.get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(self.SQL_INSERT_ENTRY, (
|
||||
self.sql_execute("log_insert_entry.sql",
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created)),
|
||||
record.name,
|
||||
record.levelno,
|
||||
@ -39,7 +22,4 @@ class MySQLLogHandler(logging.Handler):
|
||||
logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "",
|
||||
record.process,
|
||||
record.thread,
|
||||
record.threadName))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
record.threadName)
|
||||
|
@ -1,7 +1,9 @@
|
||||
|
||||
import click
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from certidude import config
|
||||
|
||||
|
||||
@ -9,13 +11,29 @@ def publish(event_type, event_data):
|
||||
"""
|
||||
Publish event on push server
|
||||
"""
|
||||
if not isinstance(event_data, str):
|
||||
if not isinstance(event_data, basestring):
|
||||
from certidude.decorators import MyEncoder
|
||||
event_data = json.dumps(event_data, cls=MyEncoder)
|
||||
|
||||
notification = requests.post(
|
||||
config.PUSH_PUBLISH % config.PUSH_TOKEN,
|
||||
data=event_data,
|
||||
headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"})
|
||||
url = config.PUSH_PUBLISH % config.PUSH_TOKEN
|
||||
click.echo("Publishing %s event %s on %s" % (event_type, event_data, url))
|
||||
|
||||
try:
|
||||
notification = requests.post(
|
||||
url,
|
||||
data=event_data,
|
||||
headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"})
|
||||
except requests.exceptions.ConnectionError:
|
||||
click.echo("Failed to submit event to push server: %s" % repr(event_data))
|
||||
|
||||
class PushLogHandler(logging.Handler):
|
||||
"""
|
||||
To be used with Python log handling framework for publishing log entries
|
||||
"""
|
||||
def emit(self, record):
|
||||
from certidude.push import publish
|
||||
publish("log-entry", dict(
|
||||
created = datetime.fromtimestamp(record.created),
|
||||
message = record.msg % record.args,
|
||||
severity = record.levelname.lower()))
|
||||
|
||||
|
103
certidude/relational.py
Normal file
@ -0,0 +1,103 @@
|
||||
|
||||
|
||||
import click
|
||||
import re
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
SCRIPTS = {}
|
||||
|
||||
class RelationalMixin(object):
|
||||
"""
|
||||
Thin wrapper around SQLite and MySQL database connectors
|
||||
"""
|
||||
|
||||
SQL_CREATE_TABLES = ""
|
||||
|
||||
def __init__(self, uri):
|
||||
self.uri = urlparse(uri)
|
||||
if self.SQL_CREATE_TABLES and self.SQL_CREATE_TABLES not in SCRIPTS:
|
||||
conn = self.sql_connect()
|
||||
cur = conn.cursor()
|
||||
with open(self.sql_resolve_script(self.SQL_CREATE_TABLES)) as fh:
|
||||
click.echo("Executing: %s" % fh.name)
|
||||
if self.uri.scheme == "sqlite":
|
||||
cur.executescript(fh.read())
|
||||
else:
|
||||
cur.execute(fh.read())
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def sql_connect(self):
|
||||
if self.uri.scheme == "mysql":
|
||||
import mysql.connector
|
||||
return mysql.connector.connect(
|
||||
user=self.uri.username,
|
||||
password=self.uri.password,
|
||||
host=self.uri.hostname,
|
||||
database=self.uri.path[1:])
|
||||
elif self.uri.scheme == "sqlite":
|
||||
if self.uri.netloc:
|
||||
raise ValueError("Malformed database URI %s" % self.uri)
|
||||
import sqlite3
|
||||
return sqlite3.connect(self.uri.path)
|
||||
else:
|
||||
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme)
|
||||
|
||||
|
||||
def sql_resolve_script(self, filename):
|
||||
return os.path.realpath(os.path.join(os.path.dirname(__file__),
|
||||
"sql", self.uri.scheme, filename))
|
||||
|
||||
|
||||
def sql_load(self, filename):
|
||||
if filename in SCRIPTS:
|
||||
return SCRIPTS[filename]
|
||||
|
||||
fh = open(self.sql_resolve_script(filename))
|
||||
click.echo("Caching SQL script: %s" % fh.name)
|
||||
buf = re.sub("\s*\n\s*", " ", fh.read())
|
||||
SCRIPTS[filename] = buf
|
||||
fh.close()
|
||||
return buf
|
||||
|
||||
|
||||
def sql_execute(self, script, *args):
|
||||
conn = self.sql_connect()
|
||||
cursor = conn.cursor()
|
||||
click.echo("Executing %s with %s" % (script, args))
|
||||
cursor.execute(self.sql_load(script), args)
|
||||
rowid = cursor.lastrowid
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return rowid
|
||||
|
||||
|
||||
def iterfetch(self, query, *args):
|
||||
conn = self.sql_connect()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, args)
|
||||
cols = [j[0] for j in cursor.description]
|
||||
def g():
|
||||
for row in cursor:
|
||||
yield dict(zip(cols, row))
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return tuple(g())
|
||||
|
||||
|
||||
def sql_fetchone(self, query, *args):
|
||||
conn = self.sql_connect()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query, args)
|
||||
cols = [j[0] for j in cursor.description]
|
||||
|
||||
for row in cursor:
|
||||
r = dict(zip(cols, row))
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return r
|
||||
return None
|
@ -6,6 +6,7 @@ import socket
|
||||
import os
|
||||
import asyncore
|
||||
import asynchat
|
||||
from certidude import constants, config
|
||||
from datetime import datetime
|
||||
from OpenSSL import crypto
|
||||
|
||||
@ -26,86 +27,83 @@ certificate authoirty (basicConstraints=CA:TRUE) or
|
||||
TLS server certificates (extendedKeyUsage=serverAuth).
|
||||
"""
|
||||
|
||||
EXTENSION_WHITELIST = set(["subjectAltName"])
|
||||
|
||||
def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usage=None, extended_key_usage=None):
|
||||
"""
|
||||
Sign certificate signing request directly with private key assuming it's readable by the process
|
||||
"""
|
||||
"""
|
||||
Sign certificate signing request directly with private key assuming it's readable by the process
|
||||
"""
|
||||
|
||||
# Initialize X.509 certificate object
|
||||
cert = crypto.X509()
|
||||
cert.set_version(2) # This corresponds to X.509v3
|
||||
# Initialize X.509 certificate object
|
||||
cert = crypto.X509()
|
||||
cert.set_version(2) # This corresponds to X.509v3
|
||||
|
||||
# Set public key
|
||||
cert.set_pubkey(request.get_pubkey())
|
||||
# Set public key
|
||||
cert.set_pubkey(request.get_pubkey())
|
||||
|
||||
# Set issuer
|
||||
cert.set_issuer(ca_cert.get_subject())
|
||||
# Set issuer
|
||||
cert.set_issuer(ca_cert.get_subject())
|
||||
|
||||
# TODO: Assert openssl.cnf policy for subject attributes
|
||||
# if request.get_subject().O != ca_cert.get_subject().O:
|
||||
# raise ValueError("Orgnization name mismatch!")
|
||||
# if request.get_subject().C != ca_cert.get_subject().C:
|
||||
# raise ValueError("Country mismatch!")
|
||||
# Copy attributes from CA
|
||||
if ca_cert.get_subject().C:
|
||||
cert.get_subject().C = ca_cert.get_subject().C
|
||||
if ca_cert.get_subject().ST:
|
||||
cert.get_subject().ST = ca_cert.get_subject().ST
|
||||
if ca_cert.get_subject().L:
|
||||
cert.get_subject().L = ca_cert.get_subject().L
|
||||
if ca_cert.get_subject().O:
|
||||
cert.get_subject().O = ca_cert.get_subject().O
|
||||
|
||||
# Copy attributes from CA
|
||||
if ca_cert.get_subject().C:
|
||||
cert.get_subject().C = ca_cert.get_subject().C
|
||||
if ca_cert.get_subject().ST:
|
||||
cert.get_subject().ST = ca_cert.get_subject().ST
|
||||
if ca_cert.get_subject().L:
|
||||
cert.get_subject().L = ca_cert.get_subject().L
|
||||
if ca_cert.get_subject().O:
|
||||
cert.get_subject().O = ca_cert.get_subject().O
|
||||
# Copy attributes from request
|
||||
cert.get_subject().CN = request.get_subject().CN
|
||||
|
||||
# Copy attributes from request
|
||||
cert.get_subject().CN = request.get_subject().CN
|
||||
req_subject = request.get_subject()
|
||||
if hasattr(req_subject, "OU") and req_subject.OU:
|
||||
cert.get_subject().OU = req_subject.OU
|
||||
if request.get_subject().SN:
|
||||
cert.get_subject().SN = request.get_subject().SN
|
||||
if request.get_subject().GN:
|
||||
cert.get_subject().GN = request.get_subject().GN
|
||||
|
||||
# Copy e-mail, key usage, extended key from request
|
||||
for extension in request.get_extensions():
|
||||
cert.add_extensions([extension])
|
||||
if request.get_subject().OU:
|
||||
cert.get_subject().OU = req_subject.OU
|
||||
|
||||
# TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request
|
||||
# Copy e-mail, key usage, extended key from request
|
||||
for extension in request.get_extensions():
|
||||
cert.add_extensions([extension])
|
||||
|
||||
# Override basic constraints if nececssary
|
||||
if basic_constraints:
|
||||
# TODO: Set keyUsage and extendedKeyUsage defaults if none has been provided in the request
|
||||
|
||||
# Override basic constraints if nececssary
|
||||
if basic_constraints:
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints",
|
||||
True,
|
||||
basic_constraints.encode("ascii"))])
|
||||
|
||||
if key_usage:
|
||||
try:
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints",
|
||||
b"keyUsage",
|
||||
True,
|
||||
basic_constraints.encode("ascii"))])
|
||||
key_usage.encode("ascii"))])
|
||||
except crypto.Error:
|
||||
raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage)
|
||||
|
||||
if key_usage:
|
||||
try:
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"keyUsage",
|
||||
True,
|
||||
key_usage.encode("ascii"))])
|
||||
except crypto.Error:
|
||||
raise ValueError("Invalid value '%s' for keyUsage attribute" % key_usage)
|
||||
if extended_key_usage:
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"extendedKeyUsage",
|
||||
True,
|
||||
extended_key_usage.encode("ascii"))])
|
||||
|
||||
if extended_key_usage:
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"extendedKeyUsage",
|
||||
True,
|
||||
extended_key_usage.encode("ascii"))])
|
||||
# Set certificate lifetime
|
||||
cert.gmtime_adj_notBefore(-3600)
|
||||
cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60)
|
||||
|
||||
# Set certificate lifetime
|
||||
cert.gmtime_adj_notBefore(-3600)
|
||||
cert.gmtime_adj_notAfter(lifetime * 24 * 60 * 60)
|
||||
|
||||
# Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff
|
||||
cert.set_serial_number(random.randint(
|
||||
0x1000000000000000000000000000000000000000,
|
||||
0xffffffffffffffffffffffffffffffffffffffff))
|
||||
cert.sign(private_key, 'sha1')
|
||||
return cert
|
||||
# Generate serial from 0x10000000000000000000 to 0xffffffffffffffffffff
|
||||
cert.set_serial_number(random.randint(
|
||||
0x1000000000000000000000000000000000000000,
|
||||
0xffffffffffffffffffffffffffffffffffffffff))
|
||||
cert.sign(private_key, 'sha256')
|
||||
return cert
|
||||
|
||||
|
||||
class SignHandler(asynchat.async_chat):
|
||||
@ -128,7 +126,7 @@ class SignHandler(asynchat.async_chat):
|
||||
serial_number, timestamp = line.split(":")
|
||||
# TODO: Assert serial against regex
|
||||
revocation = crypto.Revoked()
|
||||
revocation.set_rev_date(datetime.fromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii"))
|
||||
revocation.set_rev_date(datetime.utcfromtimestamp(int(timestamp)).strftime("%Y%m%d%H%M%SZ").encode("ascii"))
|
||||
revocation.set_reason(b"keyCompromise")
|
||||
revocation.set_serial(serial_number.encode("ascii"))
|
||||
crl.add_revoked(revocation)
|
||||
@ -137,7 +135,7 @@ class SignHandler(asynchat.async_chat):
|
||||
self.server.certificate,
|
||||
self.server.private_key,
|
||||
crypto.FILETYPE_PEM,
|
||||
self.server.revocation_list_lifetime))
|
||||
config.REVOCATION_LIST_LIFETIME))
|
||||
|
||||
elif cmd == "ocsp-request":
|
||||
NotImplemented # TODO: Implement OCSP
|
||||
@ -147,7 +145,7 @@ class SignHandler(asynchat.async_chat):
|
||||
|
||||
for e in request.get_extensions():
|
||||
key = e.get_short_name().decode("ascii")
|
||||
if key not in EXTENSION_WHITELIST:
|
||||
if key not in constants.EXTENSION_WHITELIST:
|
||||
raise ValueError("Certificte Signing Request contains extension '%s' which is not whitelisted" % key)
|
||||
|
||||
# TODO: Potential exploits during PEM parsing?
|
||||
@ -155,10 +153,10 @@ class SignHandler(asynchat.async_chat):
|
||||
self.server.private_key,
|
||||
self.server.certificate,
|
||||
request,
|
||||
basic_constraints=self.server.basic_constraints,
|
||||
key_usage=self.server.key_usage,
|
||||
extended_key_usage=self.server.extended_key_usage,
|
||||
lifetime=self.server.lifetime)
|
||||
basic_constraints=config.CERTIFICATE_BASIC_CONSTRAINTS,
|
||||
key_usage=config.CERTIFICATE_KEY_USAGE_FLAGS,
|
||||
extended_key_usage=config.CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS,
|
||||
lifetime=config.CERTIFICATE_LIFETIME)
|
||||
self.send(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
else:
|
||||
raise NotImplementedError("Unknown command: %s" % cmd)
|
||||
@ -175,26 +173,23 @@ class SignHandler(asynchat.async_chat):
|
||||
|
||||
|
||||
class SignServer(asyncore.dispatcher):
|
||||
def __init__(self, socket_path, private_key, certificate, lifetime, basic_constraints, key_usage, extended_key_usage, revocation_list_lifetime):
|
||||
def __init__(self):
|
||||
asyncore.dispatcher.__init__(self)
|
||||
|
||||
# Bind to sockets
|
||||
if os.path.exists(socket_path):
|
||||
os.unlink(socket_path)
|
||||
if os.path.exists(config.SIGNER_SOCKET_PATH):
|
||||
os.unlink(config.SIGNER_SOCKET_PATH)
|
||||
os.umask(0o007)
|
||||
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.bind(socket_path)
|
||||
self.bind(config.SIGNER_SOCKET_PATH)
|
||||
self.listen(5)
|
||||
|
||||
# Load CA private key and certificate
|
||||
self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, open(private_key).read())
|
||||
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate).read())
|
||||
self.lifetime = lifetime
|
||||
self.revocation_list_lifetime = revocation_list_lifetime
|
||||
self.basic_constraints = basic_constraints
|
||||
self.key_usage = key_usage
|
||||
self.extended_key_usage = extended_key_usage
|
||||
|
||||
# Load CA private key and certificate
|
||||
self.private_key = crypto.load_privatekey(crypto.FILETYPE_PEM,
|
||||
open(config.AUTHORITY_PRIVATE_KEY_PATH).read())
|
||||
self.certificate = crypto.load_certificate(crypto.FILETYPE_PEM,
|
||||
open(config.AUTHORITY_CERTIFICATE_PATH).read())
|
||||
|
||||
# Perhaps perform chroot as well, currently results in
|
||||
# (<class 'OpenSSL.crypto.Error'>:[('random number generator', 'SSLEAY_RAND_BYTES', 'PRNG not seeded')
|
||||
|
27
certidude/sql/mysql/log_insert_entry.sql
Normal file
@ -0,0 +1,27 @@
|
||||
insert into log (
|
||||
created,
|
||||
facility,
|
||||
level,
|
||||
severity,
|
||||
message,
|
||||
module,
|
||||
func,
|
||||
lineno,
|
||||
exception,
|
||||
process,
|
||||
thread,
|
||||
thread_name
|
||||
) values (
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s
|
||||
);
|
14
certidude/sql/mysql/log_tables.sql
Normal file
@ -0,0 +1,14 @@
|
||||
create table if not exists log (
|
||||
created datetime,
|
||||
facility varchar(30),
|
||||
level int,
|
||||
severity varchar(10),
|
||||
message text,
|
||||
module varchar(20),
|
||||
func varchar(20),
|
||||
lineno int,
|
||||
exception text,
|
||||
process int,
|
||||
thread text,
|
||||
thread_name text
|
||||
)
|
9
certidude/sql/mysql/tag_insert.sql
Normal file
@ -0,0 +1,9 @@
|
||||
insert into tag (
|
||||
`cn`,
|
||||
`key`,
|
||||
`value`
|
||||
) values (
|
||||
%s,
|
||||
%s,
|
||||
%s
|
||||
)
|
0
certidude/sql/mysql/tag_tables.sql
Normal file
27
certidude/sql/sqlite/log_insert_entry.sql
Normal file
@ -0,0 +1,27 @@
|
||||
insert into log (
|
||||
created,
|
||||
facility,
|
||||
level,
|
||||
severity,
|
||||
message,
|
||||
module,
|
||||
func,
|
||||
lineno,
|
||||
exception,
|
||||
process,
|
||||
thread,
|
||||
thread_name
|
||||
) values (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
);
|
14
certidude/sql/sqlite/log_tables.sql
Normal file
@ -0,0 +1,14 @@
|
||||
create table if not exists log (
|
||||
created datetime,
|
||||
facility varchar(30),
|
||||
level int,
|
||||
severity varchar(10),
|
||||
message text,
|
||||
module varchar(20),
|
||||
func varchar(20),
|
||||
lineno int,
|
||||
exception text,
|
||||
process int,
|
||||
thread text,
|
||||
thread_name text
|
||||
)
|
3
certidude/sql/sqlite/tag_delete.sql
Normal file
@ -0,0 +1,3 @@
|
||||
delete from tag
|
||||
where id = ?
|
||||
limit 1
|
9
certidude/sql/sqlite/tag_insert.sql
Normal file
@ -0,0 +1,9 @@
|
||||
insert into tag (
|
||||
`cn`,
|
||||
`key`,
|
||||
`value`
|
||||
) values (
|
||||
?,
|
||||
?,
|
||||
?
|
||||
);
|
16
certidude/sql/sqlite/tag_list.sql
Normal file
@ -0,0 +1,16 @@
|
||||
select
|
||||
device_tag.id as `id`,
|
||||
tag.key as `key`,
|
||||
tag.value as `value`,
|
||||
device.cn as `cn`
|
||||
from
|
||||
device_tag
|
||||
join
|
||||
tag
|
||||
on
|
||||
device_tag.tag_id = tag.id
|
||||
join
|
||||
device
|
||||
on
|
||||
device_tag.device_id = device.id
|
||||
|
36
certidude/sql/sqlite/tag_tables.sql
Normal file
@ -0,0 +1,36 @@
|
||||
create table if not exists `tag` (
|
||||
`id` integer primary key,
|
||||
`cn` varchar(255) not null,
|
||||
`key` varchar(255) not null,
|
||||
`value` varchar(255) not null
|
||||
);
|
||||
|
||||
create table if not exists `tag_properties` (
|
||||
`id` integer primary key,
|
||||
`tag_key` varchar(255) not null,
|
||||
`tag_value` varchar(255) not null,
|
||||
`property_key` varchar(255) not null,
|
||||
`property_value` varchar(255) not null
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
create table if not exists `device_tag` (
|
||||
`id` int(11) not null,
|
||||
`device_id` varchar(45) not null,
|
||||
`tag_id` varchar(45) not null,
|
||||
`attached` timestamp null default current_timestamp,
|
||||
primary key (`id`)
|
||||
);
|
||||
|
||||
create table if not exists `device` (
|
||||
`id` int(11) not null,
|
||||
`created` timestamp not null default current_timestamp,
|
||||
`cn` varchar(255) not null,
|
||||
`product_model` varchar(50) not null,
|
||||
`product_serial` varchar(50) default null,
|
||||
`hardware_address` varchar(17) unique not null,
|
||||
primary key (`id`)
|
||||
);
|
||||
|
||||
*/
|
4
certidude/sql/sqlite/tag_update.sql
Normal file
@ -0,0 +1,4 @@
|
||||
update `tag`
|
||||
set `value` = ?
|
||||
where `id` = ?
|
||||
limit 1
|
@ -29,12 +29,6 @@ img {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#pending_requests .notify {
|
||||
display: none;
|
||||
}
|
||||
@ -142,7 +136,17 @@ pre {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#container li {
|
||||
#signed ul,
|
||||
#requests ul,
|
||||
#log ul {
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#signed li,
|
||||
#requests li,
|
||||
#log li {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
clear: both;
|
||||
@ -164,7 +168,8 @@ pre {
|
||||
|
||||
.icon{
|
||||
background-size: 24px;
|
||||
padding-left: 36px;
|
||||
background-position: 6px 2px;
|
||||
padding-left: 32px;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
vertical-align: text-bottom;
|
||||
@ -172,7 +177,7 @@ pre {
|
||||
}
|
||||
|
||||
#log_entries li span.icon {
|
||||
background-size: 32px;
|
||||
background-size: 24px;
|
||||
padding-left: 42px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
@ -180,7 +185,8 @@ pre {
|
||||
|
||||
.tags .tag {
|
||||
display: inline;
|
||||
background-size: 32px;
|
||||
background-size: 24px;
|
||||
background-position: 0 4px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
padding-right: 1em;
|
||||
@ -199,25 +205,25 @@ select {
|
||||
|
||||
}
|
||||
|
||||
.icon.tag { background-image: url("../img/iconmonstr-tag-2-icon.svg"); }
|
||||
.icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); }
|
||||
|
||||
.icon.critical { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
|
||||
.icon.error { background-image: url("../img/iconmonstr-error-4-icon.svg"); }
|
||||
.icon.warning { background-image: url("../img/iconmonstr-warning-6-icon.svg"); }
|
||||
.icon.info { background-image: url("../img/iconmonstr-info-6-icon.svg"); }
|
||||
.icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); }
|
||||
.icon.error { background-image: url("../img/iconmonstr-error-4.svg"); }
|
||||
.icon.warning { background-image: url("../img/iconmonstr-warning-8.svg"); }
|
||||
.icon.info { background-image: url("../img/iconmonstr-info-8.svg"); }
|
||||
|
||||
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); }
|
||||
.icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); }
|
||||
.icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); }
|
||||
.icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); }
|
||||
.icon.revoke { background-image: url("../img/iconmonstr-x-mark-8.svg"); }
|
||||
.icon.download { background-image: url("../img/iconmonstr-download-12.svg"); }
|
||||
.icon.sign { background-image: url("../img/iconmonstr-pen-14.svg"); }
|
||||
.icon.search { background-image: url("../img/iconmonstr-magnifier-4.svg"); }
|
||||
|
||||
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-6-icon.svg"); }
|
||||
.icon.location { background-image: url("../img/iconmonstr-compass-7-icon.svg"); }
|
||||
.icon.room { background-image: url("../img/iconmonstr-home-4-icon.svg"); }
|
||||
.icon.serial { background-image: url("../img/iconmonstr-barcode-4-icon.svg"); }
|
||||
.icon.phone { background-image: url("../img/iconmonstr-mobile-phone-7.svg"); }
|
||||
.icon.location { background-image: url("../img/iconmonstr-compass-7.svg"); }
|
||||
.icon.room { background-image: url("../img/iconmonstr-home-7.svg"); }
|
||||
.icon.serial { background-image: url("../img/iconmonstr-barcode-4.svg"); }
|
||||
|
||||
.icon.wireless { background-image: url("../img/iconmonstr-wireless-6-icon.svg"); }
|
||||
.icon.password { background-image: url("../img/iconmonstr-lock-3-icon.svg"); }
|
||||
.icon.wireless { background-image: url("../img/iconmonstr-wireless-6.svg"); }
|
||||
.icon.password { background-image: url("../img/iconmonstr-lock-3.svg"); }
|
||||
|
||||
/* Make sure this is the last one */
|
||||
.icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");}
|
||||
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- License Agreement at http://iconmonstr.com/license/ -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="barcode-4-icon" fill-rule="evenodd" clip-rule="evenodd" d="M117,331.114V181.956h23.215v149.158H117z M162.318,331.114
|
||||
V181.956h27.75v149.158H162.318z M211.979,331.114V181.956h13.611v149.16L211.979,331.114z M297.614,331.114V181.956h13.61v149.16
|
||||
L297.614,331.114z M247.827,331.114l-0.001-149.158h27.749v149.16L247.827,331.114z M333.566,331.114v-149.16h23.217v149.16H333.566
|
||||
z M377.978,331.114v-149.16L395,181.956v149.158H377.978z M165.095,141.45H241v-30h-75.905V141.45z M345.566,111.45H269v30h76.566
|
||||
V111.45z M241,400.55v-30h-75.905v30H241z M462,224.863h-30v68h30V224.863z M373.566,141.45H432v55.413h30V111.45h-88.434V141.45z
|
||||
M345.566,370.55H269v30h76.566V370.55z M137.095,370.55H80v-49.687H50v79.687h87.095V370.55z M432,320.863v49.687h-58.434v30H462
|
||||
v-79.687H432z M50,292.863h30v-76H50V292.863z M80,188.863V141.45h57.095v-30H50v77.413H80z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
1
certidude/static/img/iconmonstr-barcode-4.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4 16v-8h2v8h-2zm12 0v-8h2v8h-2zm-9 0v-8h1v8h-1zm2 0v-8h2v8h-2zm3 0v-8h1v8h-1zm2 0v-8h1v8h-1zm5 0v-8h1v8h-1zm1-10h2v2h2v-4h-4v2zm-18 2v-2h2v-2h-4v4h2zm2 10h-2v-2h-2v4h4v-2zm18-2v2h-2v2h4v-4h-2zm-20-6h-2v4h2v-4zm22 0h-2v4h2v-4zm-13-6h-5v2h5v-2zm7 0h-5v2h5v-2zm-7 14h-5v2h5v-2zm7 0h-5v2h5v-2z"/></svg>
|
After Width: | Height: | Size: 391 B |
1
certidude/static/img/iconmonstr-calendar-6.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 2v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2zm1 11.729l.855-.791c1 .484 1.635.852 2.76 1.654 2.113-2.399 3.511-3.616 6.106-5.231l.279.64c-2.141 1.869-3.709 3.949-5.967 7.999-1.393-1.64-2.322-2.686-4.033-4.271z"/></svg>
|
After Width: | Height: | Size: 516 B |
@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- License Agreement at http://iconmonstr.com/license/ -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path id="certificate-15" d="M374.021,384.08c-4.527,29.103-16.648,55.725-36.043,77.92c-1.125-7.912-4.359-15.591-7.428-21.727
|
||||
c-7.023,3.705-15.439,5.666-22.799,5.666c-1.559,0-3.102-0.084-4.543-0.268c20.586-21.459,30.746-43.688,33.729-73.294
|
||||
c4.828,1.341,10.697,2.046,18.072,2.046C362.119,379.285,364.918,382.319,374.021,384.08z M457.709,445.672
|
||||
c-20.553-21.425-30.596-43.755-33.596-73.327c-4.861,1.358-10.73,2.079-18.207,2.079c-7.107,4.895-10.074,7.93-18.994,9.639
|
||||
c4.527,29.12,16.648,55.742,36.027,77.938c1.123-7.912,4.359-15.591,7.426-21.727C439.133,444.9,449.795,446.678,457.709,445.672z
|
||||
M372.01,362.789c-12.088-8.482-9.473-7.678-24.426-7.628c-0.018,0-0.018,0-0.033,0c-6.221,0-11.752-3.872-13.631-9.572
|
||||
c-4.576-13.68-3.018-11.551-15.088-19.95c-5.18-3.57-7.174-9.907-5.264-15.456c4.695-13.612,4.695-10.997,0-24.677
|
||||
c-1.877-5.499,0.033-11.869,5.264-15.457c12.07-8.383,10.496-6.27,15.088-19.958c1.879-5.717,7.41-9.564,13.631-9.564
|
||||
c0.016,0,0.016,0,0.033,0c14.938,0.042,12.322,0.888,24.426-7.628c2.514-1.76,5.465-2.649,8.449-2.649s5.934,0.889,8.449,2.649
|
||||
c12.086,8.491,9.471,7.678,24.426,7.628c0.016,0,0.016,0,0.016,0c6.236,0,11.77,3.847,13.68,9.564
|
||||
c4.561,13.654,2.951,11.542,15.055,19.958c3.822,2.632,5.969,6.822,5.969,11.165c0,1.425-0.234,2.884-0.721,4.292
|
||||
c-4.678,13.612-4.678,10.997,0,24.677c1.91,5.432,0,11.835-5.248,15.456c-12.104,8.399-10.494,6.287-15.055,19.95
|
||||
c-3.52,10.562-11.266,9.522-20.25,9.522c-7.947,0-7.98,0.721-17.871,7.678C383.879,366.326,377.039,366.326,372.01,362.789z
|
||||
M380.459,331.641c18.676,0,33.797-15.154,33.797-33.797c0-18.676-15.121-33.797-33.797-33.797s-33.797,15.121-33.797,33.797
|
||||
C346.662,316.486,361.783,331.641,380.459,331.641z M300.225,354.508c-28.76,18.172-61.131,38.574-67.837,42.799
|
||||
c-0.737-13.261-5.649-25.6-14.216-35.792c-0.998-1.257-99.79-127.031-123.981-157.987c-19.044-24.358-1.039-50.352,21.106-50.352
|
||||
c29.078,0,40.662,37.887,15.348,54.3l19.967,25.515l138.247-78.122c23.975-17.712,30.73-50.436,15.691-76.119
|
||||
C294.156,61.014,274.91,50,254.348,50c-8.155,0-16.068,1.677-23.57,5.013L88.918,127.577C66.58,138.281,54.292,159.27,54.292,181.6
|
||||
c0,14.015,4.836,28.55,15.062,41.408c24.786,31.165,124.643,158.859,125.641,160.133c14.794,19.682,0.293,47.259-23.621,47.259
|
||||
c-16.974,0-26.019-12.104-28.608-22.447c-3.018-12.104,1.19-24.157,13.269-31.903l-19.58-25.028
|
||||
c-14.686,10.327-24.032,26.001-25.876,43.521C106.646,431.857,136.386,462,171.633,462c10.821,0,21.542-2.984,31.014-8.617
|
||||
l94.158-59.379C301.33,386.896,305.891,369.461,300.225,354.508z M243.25,84.057c3.487-1.635,7.401-2.49,11.315-2.49
|
||||
c9.909,0,18.577,5.23,23.161,14.007c5.801,11.073,4.191,27.3-10.193,35.548l-91.114,51.609c0-20.453-9.975-39.212-26.957-50.67
|
||||
L243.25,84.057z M277.35,191.642c5.139,6.32,16.891,20.729,29.613,36.336c5.969-9.019,14.736-15.817,25.062-19.245
|
||||
c-11.549-14.166-21.775-26.739-26.805-32.883L277.35,191.642z M227.81,329.729l49.288-27.963l-10.863-14.149l-49.145,28.5
|
||||
L227.81,329.729z M259.428,209.772l-86.042,50.52l10.712,13.596l86.288-50.662L259.428,209.772z M281.516,237.182l-86.429,50.905
|
||||
l10.713,13.597l86.679-51.048L281.516,237.182z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.5 KiB |
1
certidude/static/img/iconmonstr-certificate-15.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18.625 19.46c-.264 1.696-.97 3.247-2.1 4.54-.065-.461-.254-.908-.433-1.266-.409.216-.899.33-1.328.33l-.265-.016c1.199-1.25 1.791-2.544 1.965-4.27.281.079.623.12 1.053.12.415.284.578.46 1.108.562zm4.875 3.589c-1.197-1.248-1.782-2.549-1.957-4.271-.283.079-.625.122-1.061.122-.414.285-.587.461-1.106.561.264 1.697.97 3.247 2.099 4.54.065-.461.254-.908.433-1.266.51.269 1.131.372 1.592.314zm-4.992-4.829c-.704-.494-.552-.447-1.423-.444h-.002c-.362 0-.685-.225-.794-.557-.267-.797-.176-.673-.879-1.163-.302-.208-.418-.577-.307-.9.273-.793.273-.641 0-1.438-.109-.32.002-.691.307-.9.703-.488.611-.365.879-1.163.109-.333.432-.557.794-.557h.002c.87.002.718.052 1.423-.444.146-.102.318-.154.492-.154s.346.052.492.154c.704.495.552.447 1.423.444h.001c.363 0 .686.224.797.557.266.796.172.673.877 1.163.223.153.348.397.348.65l-.042.25c-.272.793-.272.641 0 1.438.111.317 0 .69-.306.9-.705.489-.611.366-.877 1.163-.205.614-.656.555-1.18.555-.463 0-.465.042-1.041.446-.293.207-.691.207-.984 0zm.492-1.814c1.088 0 1.969-.882 1.969-1.969 0-1.087-.881-1.969-1.969-1.969s-1.969.881-1.969 1.969c0 1.087.881 1.969 1.969 1.969zm-4.674 1.333c-1.675 1.058-3.561 2.247-3.952 2.493-.043-.772-.329-1.492-.828-2.084-.058-.074-5.813-7.4-7.222-9.204-1.109-1.42-.06-2.934 1.23-2.934 1.694 0 2.369 2.207.894 3.163l1.163 1.486 8.053-4.551c1.396-1.032 1.79-2.938.914-4.434-.605-1.032-1.726-1.674-2.924-1.674-.475 0-.936.098-1.373.292l-8.264 4.227c-1.301.624-2.017 1.846-2.017 3.147 0 .816.282 1.663.877 2.412 1.444 1.815 7.261 9.253 7.319 9.328.862 1.147.017 2.753-1.376 2.753-.989 0-1.516-.705-1.667-1.308-.176-.705.069-1.407.773-1.858l-1.141-1.458c-.855.602-1.4 1.515-1.507 2.536-.228 2.174 1.504 3.929 3.557 3.929.63 0 1.255-.174 1.807-.502l5.485-3.458c.264-.415.529-1.431.199-2.301zm-3.319-15.755c.203-.095.431-.145.659-.145.577 0 1.082.305 1.349.816.338.645.244 1.59-.594 2.071l-5.307 3.006c0-1.191-.581-2.284-1.57-2.952l5.463-2.796zm1.987 6.267l1.725 2.117c.348-.525.858-.921 1.46-1.121l-1.562-1.916-1.623.92zm-2.886 8.043l2.871-1.628-.633-.825-2.863 1.661.625.792zm1.842-6.987l-5.012 2.943.624.792 5.026-2.951-.638-.784zm1.286 1.597l-5.035 2.965.624.792 5.049-2.974-.638-.783z"/></svg>
|
After Width: | Height: | Size: 2.2 KiB |
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="compass-7-icon" d="M256,90c91.74,0,166,74.243,166,166c0,91.741-74.245,166-166,166c-91.741,0-166-74.245-166-166
|
||||
C90,164.259,164.244,90,256,90 M256,50C142.229,50,50,142.229,50,256s92.229,206,206,206s206-92.229,206-206S369.771,50,256,50z
|
||||
M197.686,216.466l-28.355-47.135l47.225,28.408C209.145,202.733,202.736,209.099,197.686,216.466z M296.709,198.612
|
||||
c6.459,4.562,12.119,10.179,16.729,16.602l29.232-45.883L296.709,198.612z M198.312,297.179l-28.982,45.492l45.416-28.936
|
||||
C208.398,309.163,202.838,303.563,198.312,297.179z M296.018,314.604l46.652,28.066l-28.117-46.74
|
||||
C309.596,303.253,303.299,309.593,296.018,314.604z M400.199,256.001l-99.238,21.998c-4.369,8.913-11.312,16.328-19.859,21.295
|
||||
L256,400.2l-25.104-100.908c-8.545-4.965-15.488-12.381-19.857-21.293l-99.238-21.998l99.238-21.999
|
||||
c4.369-8.913,11.312-16.328,19.857-21.294L256,111.8l25.104,100.908c8.545,4.966,15.488,12.381,19.857,21.294L400.199,256.001z
|
||||
M278.406,256c0-12.374-10.031-22.407-22.406-22.407S233.592,243.626,233.592,256c0,12.376,10.033,22.408,22.408,22.408
|
||||
S278.406,268.376,278.406,256z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB |
1
certidude/static/img/iconmonstr-compass-7.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1.608 9.476l-1.608-5.476-1.611 5.477c-.429.275-.775.658-1.019 1.107l-5.37 1.416 5.37 1.416c.243.449.589.833 1.019 1.107l1.611 5.477 1.618-5.479c.428-.275.771-.659 1.014-1.109l5.368-1.412-5.368-1.413c-.244-.452-.592-.836-1.024-1.111zm-1.608 4.024c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm5.25 3.75l-2.573-1.639c.356-.264.67-.579.935-.934l1.638 2.573zm-2.641-8.911l2.64-1.588-1.588 2.639c-.29-.407-.645-.761-1.052-1.051zm-5.215 7.325l-2.644 1.586 1.589-2.641c.29.408.646.764 1.055 1.055zm-1.005-6.34l-1.638-2.573 2.573 1.638c-.357.264-.672.579-.935.935z"/></svg>
|
After Width: | Height: | Size: 837 B |
@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
|
||||
<path id="download-12-icon" d="M462,246.575c0,44.318-35.928,80.246-80.246,80.246H331.58v-38.119
|
||||
|
||||
c-0.998-43.379,40.92-44.379,59.67-46.379c-27.168-33.334-70.918-48.244-104.611-48.244c-66.546,0-108.17,39.104-108.17,104.808
|
||||
|
||||
v27.935h-48.223C85.928,326.821,50,290.894,50,246.575c0-40.982,30.729-74.766,70.396-79.623
|
||||
|
||||
c2.891-42.287,49.035-66.355,85.217-45.898c19.236-30.605,53.297-50.953,92.115-50.953c57.107,0,103.932,44.033,108.375,100
|
||||
|
||||
C438.516,180.413,462,210.747,462,246.575z M301.58,288.702c0-30.761,6.053-48.484,31.926-56.837
|
||||
|
||||
c-20.066-8.452-125.037-28.815-125.037,67.021c0,32.187,0,58.909,0,58.909h-37.408l83.963,84.104l83.965-84.104H301.58
|
||||
|
||||
C301.58,357.796,301.58,315.59,301.58,288.702z"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
1
certidude/static/img/iconmonstr-download-12.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 13h4v-7h4v7h4l-6 6-6-6zm16-1c0 5.514-4.486 10-10 10s-10-4.486-10-10 4.486-10 10-10 10 4.486 10 10zm2 0c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12z"/></svg>
|
After Width: | Height: | Size: 276 B |
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
|
||||
<path id="email-2-icon" d="M49.744,103.407v305.186H50.1h411.156h1V103.407H49.744z M415.533,138.407L255.947,260.465
|
||||
|
||||
L96.473,138.407H415.533z M84.744,173.506l85.504,65.441L84.744,324.45V173.506z M85.1,373.593l113.186-113.186l57.654,44.127
|
||||
|
||||
l57.375-43.882l112.941,112.94H85.1z M427.256,325.097l-85.896-85.896l85.896-65.695V325.097z"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 982 B |
1
certidude/static/img/iconmonstr-email-2.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z"/></svg>
|
After Width: | Height: | Size: 323 B |
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="error-4-icon" d="M324.76,90L422,187.24v137.52L324.76,422H187.24L90,324.76V187.24L187.24,90H324.76 M341.328,50H170.672
|
||||
L50,170.672v170.656L170.672,462h170.656L462,341.328V170.672L341.328,50L341.328,50z M228.55,135.812h54.9v166.5h-54.9V135.812z
|
||||
M256,388.188c-16.362,0-29.625-13.264-29.625-29.625c0-16.362,13.263-29.627,29.625-29.627c16.361,0,29.625,13.265,29.625,29.627
|
||||
C285.625,374.924,272.361,388.188,256,388.188z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
1
certidude/static/img/iconmonstr-error-4.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16.143 2l5.857 5.858v8.284l-5.857 5.858h-8.286l-5.857-5.858v-8.284l5.857-5.858h8.286zm.828-2h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-6.471 6h3l-1 8h-1l-1-8zm1.5 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z"/></svg>
|
After Width: | Height: | Size: 387 B |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- License Agreement at http://iconmonstr.com/license/ -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="flag-3-icon" d="M120.204,462H74.085V50h46.119V462z M437.915,80.746c0,0-29.079,25.642-67.324,25.642
|
||||
c-60.271,0-61.627-51.923-131.596-51.923c-37.832,0-73.106,17.577-88.045,30.381c0,12.64,0,216.762,0,216.762
|
||||
c21.204-14.696,53.426-30.144,88.286-30.144c66.08,0,75.343,49.388,134.242,49.388c38.042,0,64.437-24.369,64.437-24.369V80.746z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 786 B |
1
certidude/static/img/iconmonstr-flag-3.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M4 24h-2v-24h2v24zm18-21.387s-1.621 1.43-3.754 1.43c-3.36 0-3.436-2.895-7.337-2.895-2.108 0-4.075.98-4.909 1.694v12.085c1.184-.819 2.979-1.681 4.923-1.681 3.684 0 4.201 2.754 7.484 2.754 2.122 0 3.593-1.359 3.593-1.359v-12.028z"/></svg>
|
After Width: | Height: | Size: 328 B |
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
|
||||
<path id="home-4-icon" d="M419.492,275.815v166.213H300.725v-90.33h-89.451v90.33H92.507V275.815H50L256,69.972l206,205.844H419.492
|
||||
|
||||
z M394.072,88.472h-47.917v38.311l47.917,48.023V88.472z"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 836 B |
1
certidude/static/img/iconmonstr-home-7.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 7.093v-5.093h-3v2.093l3 3zm4 5.907l-12-12-12 12h3v10h7v-5h4v5h7v-10h3zm-5 8h-3v-5h-8v5h-3v-10.26l7-6.912 7 6.99v10.182z"/></svg>
|
After Width: | Height: | Size: 224 B |
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="info-6-icon" d="M256,90.002c91.74,0,166,74.241,166,165.998c0,91.739-74.245,165.998-166,165.998
|
||||
c-91.738,0-166-74.242-166-165.998C90,164.259,164.243,90.002,256,90.002 M256,50.002C142.229,50.002,50,142.228,50,256
|
||||
c0,113.769,92.229,205.998,206,205.998c113.77,0,206-92.229,206-205.998C462,142.228,369.77,50.002,256,50.002L256,50.002z
|
||||
M252.566,371.808c-28.21,9.913-51.466-1.455-46.801-28.547c4.667-27.098,31.436-85.109,35.255-96.079
|
||||
c3.816-10.97-3.502-13.977-11.346-9.513c-4.524,2.61-11.248,7.841-17.02,12.925c-1.601-3.223-3.852-6.906-5.542-10.433
|
||||
c9.419-9.439,25.164-22.094,43.803-26.681c22.27-5.497,59.492,3.29,43.494,45.858c-11.424,30.34-19.503,51.276-24.594,66.868
|
||||
c-5.088,15.598,0.955,18.868,9.863,12.791c6.959-4.751,14.372-11.214,19.806-16.226c2.515,4.086,3.319,5.389,5.806,10.084
|
||||
C295.857,342.524,271.182,365.151,252.566,371.808z M311.016,184.127c-12.795,10.891-31.76,10.655-42.37-0.532
|
||||
c-10.607-11.181-8.837-29.076,3.955-39.969c12.794-10.89,31.763-10.654,42.37,0.525
|
||||
C325.577,155.337,323.809,173.231,311.016,184.127z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
1
certidude/static/img/iconmonstr-info-8.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.033 16.01c.564-1.789 1.632-3.932 1.821-4.474.273-.787-.211-1.136-1.74.209l-.34-.64c1.744-1.897 5.335-2.326 4.113.613-.763 1.835-1.309 3.074-1.621 4.03-.455 1.393.694.828 1.819-.211.153.25.203.331.356.619-2.498 2.378-5.271 2.588-4.408-.146zm4.742-8.169c-.532.453-1.32.443-1.761-.022-.441-.465-.367-1.208.164-1.661.532-.453 1.32-.442 1.761.022.439.466.367 1.209-.164 1.661z"/></svg>
|
After Width: | Height: | Size: 625 B |
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="key-2-icon" stroke="#000000" stroke-miterlimit="10" d="M286.529,325.486l-45.314,45.314h-43.873l0.002,43.872
|
||||
l-45.746-0.001v41.345l-100.004-0.001l150.078-150.076c-4.578-4.686-10.061-11.391-13.691-17.423L50,426.498v-40.939
|
||||
l145.736-145.736C212.174,278.996,244.713,310.705,286.529,325.486z M425.646,92.339c48.473,48.473,48.471,127.064-0.002,175.535
|
||||
c-48.477,48.476-127.061,48.476-175.537,0.001c-48.473-48.472-48.475-127.062,0-175.537
|
||||
C298.58,43.865,377.172,43.865,425.646,92.339z M400.73,117.165c-12.023-12.021-31.516-12.021-43.537,0
|
||||
c-12.021,12.022-12.021,31.517,0,43.538s31.514,12.021,43.537-0.001C412.754,148.68,412.75,129.188,400.73,117.165z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
1
certidude/static/img/iconmonstr-key-3.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12.451 17.337l-2.451 2.663h-2v2h-2v2h-6v-1.293l7.06-7.06c-.214-.26-.413-.533-.599-.815l-6.461 6.461v-2.293l6.865-6.949c1.08 2.424 3.095 4.336 5.586 5.286zm11.549-9.337c0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8 8 3.582 8 8zm-3-3c0-1.104-.896-2-2-2s-2 .896-2 2 .896 2 2 2 2-.896 2-2z"/></svg>
|
After Width: | Height: | Size: 386 B |
@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="lock-3-icon" d="M195.334,223.333h-50v-62.666C145.334,99.645,194.979,50,256,50c61.022,0,110.667,49.645,110.667,110.667
|
||||
v62.666h-50v-62.666C316.667,127.215,289.452,100,256,100c-33.451,0-60.666,27.215-60.666,60.667V223.333z M404,253.333V462H108
|
||||
V253.333H404z M283,341c0-14.912-12.088-27-27-27s-27,12.088-27,27c0,7.811,3.317,14.844,8.619,19.773
|
||||
c4.385,4.075,6.881,9.8,6.881,15.785V399.5h23v-22.941c0-5.989,2.494-11.708,6.881-15.785C279.683,355.844,283,348.811,283,341z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
|
||||
<path id="magnifier-4-icon" d="M448.225,394.243l-85.387-85.385c16.55-26.081,26.146-56.986,26.146-90.094
|
||||
|
||||
c0-92.989-75.652-168.641-168.643-168.641c-92.989,0-168.641,75.652-168.641,168.641s75.651,168.641,168.641,168.641
|
||||
|
||||
c31.465,0,60.939-8.67,86.175-23.735l86.14,86.142C429.411,486.566,485.011,431.029,448.225,394.243z M103.992,218.764
|
||||
|
||||
c0-64.156,52.192-116.352,116.35-116.352s116.353,52.195,116.353,116.352s-52.195,116.352-116.353,116.352
|
||||
|
||||
S103.992,282.92,103.992,218.764z M138.455,188.504c34.057-78.9,148.668-69.752,170.248,12.862
|
||||
|
||||
C265.221,150.329,188.719,144.834,138.455,188.504z"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
1
certidude/static/img/iconmonstr-magnifier-4.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M23.111 20.058l-4.977-4.977c.965-1.52 1.523-3.322 1.523-5.251 0-5.42-4.409-9.83-9.829-9.83-5.42 0-9.828 4.41-9.828 9.83s4.408 9.83 9.829 9.83c1.834 0 3.552-.505 5.022-1.383l5.021 5.021c2.144 2.141 5.384-1.096 3.239-3.24zm-20.064-10.228c0-3.739 3.043-6.782 6.782-6.782s6.782 3.042 6.782 6.782-3.043 6.782-6.782 6.782-6.782-3.043-6.782-6.782zm2.01-1.764c1.984-4.599 8.664-4.066 9.922.749-2.534-2.974-6.993-3.294-9.922-.749z"/></svg>
|
After Width: | Height: | Size: 522 B |
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="mobile-phone-6-icon" d="M139.59,131.775c-13.807,0-25,11.197-25,25.01V436.99c0,13.812,11.193,25.01,25,25.01h150.49
|
||||
c13.807,0,25-11.198,25-25.01V156.766c0-13.802-11.186-24.99-24.98-24.99H139.59z M179.832,416.514h-30.996v-24.51h30.996V416.514z
|
||||
M179.832,372.203h-30.996v-24.51h30.996V372.203z M230.334,416.514h-30.996v-24.51h30.996V416.514z M230.334,372.203h-30.996
|
||||
v-24.51h30.996V372.203z M280.836,416.514H249.84v-24.51h30.996V416.514z M280.836,372.203H249.84v-24.51h30.996V372.203z
|
||||
M280.836,312.887h-132V183.226h132V312.887z M283.451,111.408c13.445-0.01,26.9,5.113,37.164,15.369s15.4,23.699,15.41,37.147
|
||||
h22.121c-0.012-19.113-7.312-38.231-21.898-52.805c-14.588-14.573-33.691-21.854-52.797-21.842V111.408z M283.451,72.682
|
||||
c23.354-0.015,46.691,8.882,64.52,26.696c17.828,17.812,26.75,41.187,26.766,64.547h22.674c-0.02-29.166-11.16-58.358-33.418-80.597
|
||||
C341.734,61.089,312.605,49.982,283.451,50V72.682z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
1
certidude/static/img/iconmonstr-mobile-phone-7.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 6c-1.104 0-2 .896-2 2v14c0 1.104.896 2 2 2h8c1.104 0 2-.896 2-2v-14c0-1.104-.896-2-2-2h-8zm2 15h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm0-3h-8v-7h8v7zm0-11.688c.944-.001 1.889.359 2.608 1.08.721.72 1.082 1.664 1.082 2.606h1.554c-.001-1.341-.514-2.684-1.538-3.707-1.025-1.022-2.365-1.533-3.706-1.532v1.553zm0-2.718c1.639-.001 3.277.623 4.53 1.874 1.251 1.25 1.877 2.892 1.878 4.531h1.592c-.001-2.047-.782-4.096-2.345-5.658-1.562-1.562-3.609-2.341-5.655-2.34v1.593z"/></svg>
|
After Width: | Height: | Size: 613 B |
@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- License Agreement at http://iconmonstr.com/license/ -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="pen-10-icon" d="M244.558,199.493l67.827,67.826l-73.17,134.531c0,0-90.805,23.4-147.694,60.027l-14.185-14.182
|
||||
l68.113-68.105c5.975-5.982,13.726-9.773,22.11-10.807c4.642-0.582,9.128-2.621,12.696-6.205c8.538-8.547,8.546-22.4-0.002-30.951
|
||||
c-8.549-8.543-22.407-8.543-30.959-0.002c-3.573,3.572-5.623,8.061-6.199,12.693c-1.028,8.371-4.834,16.15-10.8,22.117
|
||||
l-68.104,68.105L50,420.354c37.028-57.496,60.021-147.693,60.021-147.693L244.558,199.493z M315.896,50.122
|
||||
c-22.784,44.143-53.014,100-53.014,100l98.872,98.869c0,0,55.909-30.086,100.246-52.766L315.896,50.122z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1016 B |
1
certidude/static/img/iconmonstr-pen-14.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12.014 6.54s2.147-3.969 3.475-6.54l8.511 8.511c-2.583 1.321-6.556 3.459-6.556 3.459l-5.43-5.43zm-8.517 6.423s-1.339 5.254-3.497 8.604l.827.826 3.967-3.967c.348-.348.569-.801.629-1.288.034-.27.153-.532.361-.74.498-.498 1.306-.498 1.803 0 .498.499.498 1.305 0 1.803-.208.209-.469.328-.74.361-.488.061-.94.281-1.288.63l-3.967 3.968.826.84c3.314-2.133 8.604-3.511 8.604-3.511l4.262-7.837-3.951-3.951-7.836 4.262z"/></svg>
|
After Width: | Height: | Size: 510 B |
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
|
||||
<path id="tag-2-icon" d="M234.508,50L50.068,50.262l-0.004,184.311L277.365,462l184.57-184.57L234.508,50z M114.877,167.365
|
||||
|
||||
c-15.027-15.027-15.027-39.395,0-54.424c15.029-15.029,39.396-15.029,54.426,0s15.029,39.396,0,54.424
|
||||
|
||||
C154.273,182.395,129.906,182.395,114.877,167.365z M242.316,327.94l-76.225-76.226l17.678-17.678l76.225,76.226L242.316,327.94z
|
||||
|
||||
M317.609,335.887L199.764,218.041l17.678-17.678l117.846,117.846L317.609,335.887z M351.818,301.678L233.973,183.832l17.678-17.678
|
||||
|
||||
L369.496,284L351.818,301.678z"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
1
certidude/static/img/iconmonstr-tag-3.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10.605 0h-10.604v10.609l13.39 13.391 10.609-10.605-13.395-13.395zm-7.019 6.414c-.781-.782-.781-2.047 0-2.828.782-.781 2.048-.781 2.828-.002.782.783.782 2.048 0 2.83-.781.781-2.046.781-2.828 0zm6.823 8.947l-4.243-4.242.708-.708 4.243 4.243-.708.707zm4.949.707l-7.07-7.071.707-.707 7.071 7.071-.708.707zm2.121-2.121l-7.071-7.071.707-.707 7.071 7.071-.707.707z"/></svg>
|
After Width: | Height: | Size: 459 B |
@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="time-13-icon" d="M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933
|
||||
c0,25.044,8.566,49.646,24.121,69.273l50.056,63.166c9.206,11.617,9.271,27.895,0.159,39.584l-50.768,65.13
|
||||
c-15.198,19.497-23.568,43.85-23.568,68.571V462h259.5v-53.343c0-24.722-8.37-49.073-23.567-68.571l-50.769-65.13
|
||||
c-9.112-11.689-9.047-27.967,0.159-39.584L361.629,172.206z M330.634,364.678c11.412,14.64,15.116,29.947,15.116,47.321h-11.096
|
||||
c-4.586-17.886-31.131-30.642-62.559-47.586c-6.907-3.724-6.096-10.373-6.096-15.205h-20c0,4.18,1.03,11.365-6.106,15.202
|
||||
c-32.073,17.249-58.274,29.705-62.701,47.589H166.25c0-17.261,3.645-32.605,15.115-47.321l50.769-65.13
|
||||
c7.109-9.12,11.723-19.484,13.866-30.22v13.38h20V269.33c2.144,10.734,6.758,21.098,13.866,30.218L330.634,364.678z
|
||||
M197.966,167.862l-16.245-20.5c-11.538-14.56-15.471-30.096-15.471-47.361h179.5c0,17.149-3.872,32.727-15.471,47.361l-16.245,20.5
|
||||
H197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.6 KiB |
1
certidude/static/img/iconmonstr-user-5.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 7.001c0 3.865-3.134 7-7 7s-7-3.135-7-7c0-3.867 3.134-7.001 7-7.001s7 3.134 7 7.001zm-1.598 7.18c-1.506 1.137-3.374 1.82-5.402 1.82-2.03 0-3.899-.685-5.407-1.822-4.072 1.793-6.593 7.376-6.593 9.821h24c0-2.423-2.6-8.006-6.598-9.819z"/></svg>
|
After Width: | Height: | Size: 335 B |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="warning-6-icon" d="M239.939,231.352h32.121v97.421h-32.121V231.352z M256,379.019c-9.574,0-17.334-7.761-17.334-17.334
|
||||
c0-9.574,7.76-17.335,17.334-17.335c9.573,0,17.334,7.761,17.334,17.335C273.334,371.258,265.573,379.019,256,379.019z M256,78.07
|
||||
L50,434.873h412L256,78.07z M256,158.07l136.718,236.803H119.282L256,158.07z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 970 B |
1
certidude/static/img/iconmonstr-warning-8.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 5.177l8.631 15.823h-17.262l8.631-15.823zm0-4.177l-12 22h24l-12-22zm-1 9h2v6h-2v-6zm1 9.75c-.689 0-1.25-.56-1.25-1.25s.561-1.25 1.25-1.25 1.25.56 1.25 1.25-.561 1.25-1.25 1.25z"/></svg>
|
After Width: | Height: | Size: 280 B |
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="wireless-6-icon" d="M50,178.599c52.72-52.72,125.552-85.328,206-85.328c80.448,0,153.28,32.608,206,85.328l-35,35
|
||||
c-43.763-43.763-104.221-70.83-171-70.83c-66.78,0-127.237,27.067-171,70.83L50,178.599z M148.196,276.796
|
||||
c27.589-27.59,65.704-44.654,107.804-44.654s80.215,17.064,107.804,44.654l35.935-35.936
|
||||
c-36.785-36.787-87.604-59.539-143.738-59.539s-106.953,22.752-143.738,59.539L148.196,276.796z M211,339.599
|
||||
c11.517-11.517,27.427-18.64,45-18.64s33.483,7.123,45,18.64l35.313-35.312c-20.554-20.554-48.949-33.269-80.313-33.269
|
||||
s-59.76,12.715-80.313,33.269L211,339.599z M256,356.138c-17.284,0-31.299,14.01-31.299,31.297
|
||||
c0,17.285,14.015,31.295,31.299,31.295c17.283,0,31.296-14.01,31.296-31.295C287.296,370.147,273.283,356.138,256,356.138z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
|
||||
width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
|
||||
<path id="x-mark-5-icon" d="M432.546,133.462L367.133,76.39L254.078,210.715L140.967,73.702l-61.513,65.068
|
||||
|
||||
c33.791,43.885,78.146,89.797,123.688,132.465L82.993,413.987l19.865,22.629c29.251-20.31,87.839-65.578,150.312-120.092
|
||||
|
||||
c63.662,55.812,122.861,101.336,151.301,121.773l21.438-19.443L303.804,270.95C352.439,225.709,399.308,177.442,432.546,133.462z"/>
|
||||
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1001 B |
1
certidude/static/img/iconmonstr-x-mark-8.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M24 3.752l-4.423-3.752-7.771 9.039-7.647-9.008-4.159 4.278c2.285 2.885 5.284 5.903 8.362 8.708l-8.165 9.447 1.343 1.487c1.978-1.335 5.981-4.373 10.205-7.958 4.304 3.67 8.306 6.663 10.229 8.006l1.449-1.278-8.254-9.724c3.287-2.973 6.584-6.354 8.831-9.245z"/></svg>
|
After Width: | Height: | Size: 354 B |
@ -14,11 +14,12 @@
|
||||
<body>
|
||||
<nav id="menu">
|
||||
<ul class="container">
|
||||
<li data-section="requests">Requests</li>
|
||||
<li data-section="signed">Signed</li>
|
||||
<li data-section="revoked">Revoked</li>
|
||||
<li data-section="config">Configuration</li>
|
||||
<li data-section="log">Log</li>
|
||||
<li data-section="about">Profile</li>
|
||||
<li id="section-requests" data-section="requests" style="display:none;">Requests</li>
|
||||
<li id="section-signed" data-section="signed" style="display:none;">Signed</li>
|
||||
<li id="section-revoked" data-section="revoked" style="display:none;">Revoked</li>
|
||||
<li id="section-config" data-section="config" style="display:none;">Configuration</li>
|
||||
<li id="section-log" data-section="log" style="display:none;">Log</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div id="container" class="container">
|
||||
|
@ -67,7 +67,6 @@ function onRequestSubmitted(e) {
|
||||
url: "/api/request/" + e.data + "/",
|
||||
dataType: "json",
|
||||
success: function(request, status, xhr) {
|
||||
console.info(request);
|
||||
$("#pending_requests").prepend(
|
||||
nunjucks.render('views/request.html', { request: request }));
|
||||
}
|
||||
@ -75,12 +74,12 @@ function onRequestSubmitted(e) {
|
||||
}
|
||||
|
||||
function onRequestDeleted(e) {
|
||||
console.log("Removing deleted request #" + e.data);
|
||||
$("#request_" + e.data).remove();
|
||||
console.log("Removing deleted request", e.data);
|
||||
$("#request-" + e.data.replace("@", "--").replace(".", "-")).remove();
|
||||
}
|
||||
|
||||
function onClientUp(e) {
|
||||
console.log("Adding security association:" + e.data);
|
||||
console.log("Adding security association:", e.data);
|
||||
var lease = JSON.parse(e.data);
|
||||
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
|
||||
$status.html(nunjucks.render('views/status.html', {
|
||||
@ -93,7 +92,7 @@ function onClientUp(e) {
|
||||
}
|
||||
|
||||
function onClientDown(e) {
|
||||
console.log("Removing security association:" + e.data);
|
||||
console.log("Removing security association:", e.data);
|
||||
var lease = JSON.parse(e.data);
|
||||
var $status = $("#signed_certificates [data-dn='" + lease.identity + "'] .status");
|
||||
$status.html(nunjucks.render('views/status.html', {
|
||||
@ -107,7 +106,9 @@ function onClientDown(e) {
|
||||
|
||||
function onRequestSigned(e) {
|
||||
console.log("Request signed:", e.data);
|
||||
$("#request_" + e.data).slideUp("normal", function() { $(this).remove(); });
|
||||
|
||||
$("#request-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
|
||||
$("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
|
||||
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
@ -121,13 +122,14 @@ function onRequestSigned(e) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function onCertificateRevoked(e) {
|
||||
console.log("Removing revoked certificate #" + e.data);
|
||||
$("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); });
|
||||
console.log("Removing revoked certificate", e.data);
|
||||
$("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
|
||||
}
|
||||
|
||||
function onTagAdded(e) {
|
||||
console.log("Tag added #" + e.data);
|
||||
console.log("Tag added", e.data);
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/tag/" + e.data + "/",
|
||||
@ -143,12 +145,12 @@ function onTagAdded(e) {
|
||||
}
|
||||
|
||||
function onTagRemoved(e) {
|
||||
console.log("Tag removed #" + e.data);
|
||||
console.log("Tag removed", e.data);
|
||||
$("#tag_" + e.data).remove();
|
||||
}
|
||||
|
||||
function onTagUpdated(e) {
|
||||
console.log("Tag updated #" + e.data);
|
||||
console.log("Tag updated", e.data);
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/tag/" + e.data + "/",
|
||||
@ -175,32 +177,49 @@ $(document).ready(function() {
|
||||
$("#container").html(nunjucks.render('views/error.html', { message: msg }));
|
||||
},
|
||||
success: function(session, status, xhr) {
|
||||
console.info("Opening EventSource from:", session.event_channel);
|
||||
|
||||
var source = new EventSource(session.event_channel);
|
||||
|
||||
source.onmessage = function(event) {
|
||||
console.log("Received server-sent event:", event);
|
||||
}
|
||||
|
||||
source.addEventListener("log-entry", onLogEntry);
|
||||
source.addEventListener("up-client", onClientUp);
|
||||
source.addEventListener("down-client", onClientDown);
|
||||
source.addEventListener("request-deleted", onRequestDeleted);
|
||||
source.addEventListener("request-submitted", onRequestSubmitted);
|
||||
source.addEventListener("request-signed", onRequestSigned);
|
||||
source.addEventListener("certificate-revoked", onCertificateRevoked);
|
||||
source.addEventListener("tag-added", onTagAdded);
|
||||
source.addEventListener("tag-removed", onTagRemoved);
|
||||
source.addEventListener("tag-updated", onTagUpdated);
|
||||
$("#login").hide();
|
||||
|
||||
/**
|
||||
* Render authority views
|
||||
**/
|
||||
$("#container").html(nunjucks.render('views/authority.html', { session: session, window: window }));
|
||||
console.info("Swtiching to requests section");
|
||||
$("section").hide();
|
||||
$("section#requests").show();
|
||||
|
||||
if (session.authority) {
|
||||
$("#log input").each(function(i, e) {
|
||||
console.info("e.checked:", e.checked , "and", e.id, "@localstorage is", localStorage[e.id], "setting to:", localStorage[e.id] || e.checked, "bool:", localStorage[e.id] || e.checked == "true");
|
||||
e.checked = localStorage[e.id] ? localStorage[e.id] == "true" : e.checked;
|
||||
});
|
||||
|
||||
$("#log input").change(function() {
|
||||
localStorage[this.id] = this.checked;
|
||||
});
|
||||
|
||||
console.info("Opening EventSource from:", session.authority.events);
|
||||
|
||||
var source = new EventSource(session.authority.events);
|
||||
|
||||
source.onmessage = function(event) {
|
||||
console.log("Received server-sent event:", event);
|
||||
}
|
||||
|
||||
source.addEventListener("log-entry", onLogEntry);
|
||||
source.addEventListener("up-client", onClientUp);
|
||||
source.addEventListener("down-client", onClientDown);
|
||||
source.addEventListener("request-deleted", onRequestDeleted);
|
||||
source.addEventListener("request-submitted", onRequestSubmitted);
|
||||
source.addEventListener("request-signed", onRequestSigned);
|
||||
source.addEventListener("certificate-revoked", onCertificateRevoked);
|
||||
source.addEventListener("tag-added", onTagAdded);
|
||||
source.addEventListener("tag-removed", onTagRemoved);
|
||||
source.addEventListener("tag-updated", onTagUpdated);
|
||||
|
||||
console.info("Swtiching to requests section");
|
||||
$("section").hide();
|
||||
$("section#requests").show();
|
||||
$("#section-revoked").show();
|
||||
$("#section-signed").show();
|
||||
$("#section-requests").show();
|
||||
}
|
||||
|
||||
$("nav#menu li").click(function(e) {
|
||||
$("section").hide();
|
||||
@ -231,88 +250,97 @@ $(document).ready(function() {
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/config/",
|
||||
dataType: "json",
|
||||
success: function(configuration, status, xhr) {
|
||||
console.info("Appending " + configuration.length + " configuration items");
|
||||
$("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
|
||||
/**
|
||||
* Fetch tags for certificates
|
||||
*/
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/tag/",
|
||||
dataType: "json",
|
||||
success:function(tags, status, xhr) {
|
||||
console.info("Got", tags.length, "tags");
|
||||
for (var j = 0; j < tags.length; j++) {
|
||||
// TODO: Deduplicate
|
||||
$tag = $("<span id=\"tag_" + tags[j].id + "\" title=\"" + tags[j].key + "=" + tags[j].value + "\" class=\"" + tags[j].key.replace(/\./g, " ") + " icon tag\" data-id=\""+tags[j].id+"\" data-key=\"" + tags[j].key + "\">" + tags[j].value + "</span>");
|
||||
console.info("Inserting tag", tags[j], $tag);
|
||||
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" ");
|
||||
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag);
|
||||
$tag.click(onTagClicked);
|
||||
$("#tags_autocomplete").prepend("<option value=\"" + tags[j].id + "\">" + tags[j].key + "='" + tags[j].value + "'</option>");
|
||||
console.log("Features enabled:", session.features);
|
||||
if (session.features.tagging) {
|
||||
console.info("Tagging enabled");
|
||||
$("#section-config").show();
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/config/",
|
||||
dataType: "json",
|
||||
success: function(configuration, status, xhr) {
|
||||
console.info("Appending", configuration.length, "configuration items");
|
||||
$("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
|
||||
/**
|
||||
* Fetch tags for certificates
|
||||
*/
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/tag/",
|
||||
dataType: "json",
|
||||
success:function(tags, status, xhr) {
|
||||
console.info("Got", tags.length, "tags");
|
||||
$("#config").html(nunjucks.render('views/configuration.html', { configuration:configuration}));
|
||||
for (var j = 0; j < tags.length; j++) {
|
||||
// TODO: Deduplicate
|
||||
$tag = $("<span id=\"tag_" + tags[j].id + "\" title=\"" + tags[j].key + "=" + tags[j].value + "\" class=\"" + tags[j].key.replace(/\./g, " ") + " icon tag\" data-id=\""+tags[j].id+"\" data-key=\"" + tags[j].key + "\">" + tags[j].value + "</span>");
|
||||
console.info("Inserting tag", tags[j], $tag);
|
||||
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend(" ");
|
||||
$tags = $("#signed_certificates [data-cn='" + tags[j].cn + "'] .tags").prepend($tag);
|
||||
$tag.click(onTagClicked);
|
||||
$("#tags_autocomplete").prepend("<option value=\"" + tags[j].id + "\">" + tags[j].key + "='" + tags[j].value + "'</option>");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch leases associated with certificates
|
||||
*/
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/lease/",
|
||||
dataType: "json",
|
||||
success: function(leases, status, xhr) {
|
||||
console.info("Got leases:", leases);
|
||||
for (var j = 0; j < leases.length; j++) {
|
||||
var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status");
|
||||
if (!$status.length) {
|
||||
console.info("Detected rogue client:", leases[j]);
|
||||
continue;
|
||||
if (session.features.leases) {
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/lease/",
|
||||
dataType: "json",
|
||||
success: function(leases, status, xhr) {
|
||||
console.info("Got leases:", leases);
|
||||
for (var j = 0; j < leases.length; j++) {
|
||||
var $status = $("#signed_certificates [data-dn='" + leases[j].identity + "'] .status");
|
||||
if (!$status.length) {
|
||||
console.info("Detected rogue client:", leases[j]);
|
||||
continue;
|
||||
}
|
||||
$status.html(nunjucks.render('views/status.html', {
|
||||
lease: {
|
||||
address: leases[j].address,
|
||||
age: (new Date() - new Date(leases[j].released)) / 1000,
|
||||
identity: leases[j].identity,
|
||||
acquired: new Date(leases[j].acquired).toLocaleString(),
|
||||
released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null
|
||||
}}));
|
||||
}
|
||||
$status.html(nunjucks.render('views/status.html', {
|
||||
lease: {
|
||||
address: leases[j].address,
|
||||
age: (new Date() - new Date(leases[j].released)) / 1000,
|
||||
identity: leases[j].identity,
|
||||
acquired: new Date(leases[j].acquired).toLocaleString(),
|
||||
released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null
|
||||
}}));
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch log entries
|
||||
*/
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/log/",
|
||||
dataType: "json",
|
||||
success:function(entries, status, xhr) {
|
||||
console.info("Got", entries.length, "log entries");
|
||||
for (var j = 0; j < entries.length; j++) {
|
||||
if ($("#log_level_" + entries[j].severity).prop("checked")) {
|
||||
$("#log_entries").append(nunjucks.render("views/logentry.html", {
|
||||
entry: {
|
||||
created: new Date(entries[j].created).toLocaleString("et-EE"),
|
||||
message: entries[j].message,
|
||||
severity: entries[j].severity
|
||||
}
|
||||
}));
|
||||
if (session.features.logging) {
|
||||
$("#section-log").show();
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/log/",
|
||||
dataType: "json",
|
||||
success:function(entries, status, xhr) {
|
||||
console.info("Got", entries.length, "log entries");
|
||||
for (var j = 0; j < entries.length; j++) {
|
||||
if ($("#log_level_" + entries[j].severity).prop("checked")) {
|
||||
$("#log_entries").append(nunjucks.render("views/logentry.html", {
|
||||
entry: {
|
||||
created: new Date(entries[j].created).toLocaleString("et-EE"),
|
||||
message: entries[j].message,
|
||||
severity: entries[j].severity
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
4
certidude/static/js/nunjucks-slim.min.js
vendored
4
certidude/static/js/nunjucks.min.js
vendored
@ -1,11 +1,11 @@
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-barcode-4-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-barcode-4.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"barcode-4-icon\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M117,331.114V181.956h23.215v149.158H117z M162.318,331.114\r\n\tV181.956h27.75v149.158H162.318z M211.979,331.114V181.956h13.611v149.16L211.979,331.114z M297.614,331.114V181.956h13.61v149.16\r\n\tL297.614,331.114z M247.827,331.114l-0.001-149.158h27.749v149.16L247.827,331.114z M333.566,331.114v-149.16h23.217v149.16H333.566\r\n\tz M377.978,331.114v-149.16L395,181.956v149.158H377.978z M165.095,141.45H241v-30h-75.905V141.45z M345.566,111.45H269v30h76.566\r\n\tV111.45z M241,400.55v-30h-75.905v30H241z M462,224.863h-30v68h30V224.863z M373.566,141.45H432v55.413h30V111.45h-88.434V141.45z\r\n\t M345.566,370.55H269v30h76.566V370.55z M137.095,370.55H80v-49.687H50v79.687h87.095V370.55z M432,320.863v49.687h-58.434v30H462\r\n\tv-79.687H432z M50,292.863h30v-76H50V292.863z M80,188.863V141.45h57.095v-30H50v77.413H80z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M4 16v-8h2v8h-2zm12 0v-8h2v8h-2zm-9 0v-8h1v8h-1zm2 0v-8h2v8h-2zm3 0v-8h1v8h-1zm2 0v-8h1v8h-1zm5 0v-8h1v8h-1zm1-10h2v2h2v-4h-4v2zm-18 2v-2h2v-2h-4v4h2zm2 10h-2v-2h-2v4h4v-2zm18-2v2h-2v2h4v-4h-2zm-20-6h-2v4h2v-4zm22 0h-2v4h2v-4zm-13-6h-5v2h5v-2zm7 0h-5v2h5v-2zm-7 14h-5v2h5v-2zm7 0h-5v2h5v-2z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -22,14 +22,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-calendar-6.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" style=\"enable-background:new 0 0 512 512;\" xml:space=\"preserve\">\r\n<path id=\"certificate-15\" d=\"M374.021,384.08c-4.527,29.103-16.648,55.725-36.043,77.92c-1.125-7.912-4.359-15.591-7.428-21.727\r\n\tc-7.023,3.705-15.439,5.666-22.799,5.666c-1.559,0-3.102-0.084-4.543-0.268c20.586-21.459,30.746-43.688,33.729-73.294\r\n\tc4.828,1.341,10.697,2.046,18.072,2.046C362.119,379.285,364.918,382.319,374.021,384.08z M457.709,445.672\r\n\tc-20.553-21.425-30.596-43.755-33.596-73.327c-4.861,1.358-10.73,2.079-18.207,2.079c-7.107,4.895-10.074,7.93-18.994,9.639\r\n\tc4.527,29.12,16.648,55.742,36.027,77.938c1.123-7.912,4.359-15.591,7.426-21.727C439.133,444.9,449.795,446.678,457.709,445.672z\r\n\t M372.01,362.789c-12.088-8.482-9.473-7.678-24.426-7.628c-0.018,0-0.018,0-0.033,0c-6.221,0-11.752-3.872-13.631-9.572\r\n\tc-4.576-13.68-3.018-11.551-15.088-19.95c-5.18-3.57-7.174-9.907-5.264-15.456c4.695-13.612,4.695-10.997,0-24.677\r\n\tc-1.877-5.499,0.033-11.869,5.264-15.457c12.07-8.383,10.496-6.27,15.088-19.958c1.879-5.717,7.41-9.564,13.631-9.564\r\n\tc0.016,0,0.016,0,0.033,0c14.938,0.042,12.322,0.888,24.426-7.628c2.514-1.76,5.465-2.649,8.449-2.649s5.934,0.889,8.449,2.649\r\n\tc12.086,8.491,9.471,7.678,24.426,7.628c0.016,0,0.016,0,0.016,0c6.236,0,11.77,3.847,13.68,9.564\r\n\tc4.561,13.654,2.951,11.542,15.055,19.958c3.822,2.632,5.969,6.822,5.969,11.165c0,1.425-0.234,2.884-0.721,4.292\r\n\tc-4.678,13.612-4.678,10.997,0,24.677c1.91,5.432,0,11.835-5.248,15.456c-12.104,8.399-10.494,6.287-15.055,19.95\r\n\tc-3.52,10.562-11.266,9.522-20.25,9.522c-7.947,0-7.98,0.721-17.871,7.678C383.879,366.326,377.039,366.326,372.01,362.789z\r\n\t M380.459,331.641c18.676,0,33.797-15.154,33.797-33.797c0-18.676-15.121-33.797-33.797-33.797s-33.797,15.121-33.797,33.797\r\n\tC346.662,316.486,361.783,331.641,380.459,331.641z M300.225,354.508c-28.76,18.172-61.131,38.574-67.837,42.799\r\n\tc-0.737-13.261-5.649-25.6-14.216-35.792c-0.998-1.257-99.79-127.031-123.981-157.987c-19.044-24.358-1.039-50.352,21.106-50.352\r\n\tc29.078,0,40.662,37.887,15.348,54.3l19.967,25.515l138.247-78.122c23.975-17.712,30.73-50.436,15.691-76.119\r\n\tC294.156,61.014,274.91,50,254.348,50c-8.155,0-16.068,1.677-23.57,5.013L88.918,127.577C66.58,138.281,54.292,159.27,54.292,181.6\r\n\tc0,14.015,4.836,28.55,15.062,41.408c24.786,31.165,124.643,158.859,125.641,160.133c14.794,19.682,0.293,47.259-23.621,47.259\r\n\tc-16.974,0-26.019-12.104-28.608-22.447c-3.018-12.104,1.19-24.157,13.269-31.903l-19.58-25.028\r\n\tc-14.686,10.327-24.032,26.001-25.876,43.521C106.646,431.857,136.386,462,171.633,462c10.821,0,21.542-2.984,31.014-8.617\r\n\tl94.158-59.379C301.33,386.896,305.891,369.461,300.225,354.508z M243.25,84.057c3.487-1.635,7.401-2.49,11.315-2.49\r\n\tc9.909,0,18.577,5.23,23.161,14.007c5.801,11.073,4.191,27.3-10.193,35.548l-91.114,51.609c0-20.453-9.975-39.212-26.957-50.67\r\n\tL243.25,84.057z M277.35,191.642c5.139,6.32,16.891,20.729,29.613,36.336c5.969-9.019,14.736-15.817,25.062-19.245\r\n\tc-11.549-14.166-21.775-26.739-26.805-32.883L277.35,191.642z M227.81,329.729l49.288-27.963l-10.863-14.149l-49.145,28.5\r\n\tL227.81,329.729z M259.428,209.772l-86.042,50.52l10.712,13.596l86.288-50.662L259.428,209.772z M281.516,237.182l-86.429,50.905\r\n\tl10.713,13.597l86.679-51.048L281.516,237.182z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 2v22h-24v-22h3v1c0 1.103.897 2 2 2s2-.897 2-2v-1h10v1c0 1.103.897 2 2 2s2-.897 2-2v-1h3zm-2 6h-20v14h20v-14zm-2-7c0-.552-.447-1-1-1s-1 .448-1 1v2c0 .552.447 1 1 1s1-.448 1-1v-2zm-14 2c0 .552-.447 1-1 1s-1-.448-1-1v-2c0-.552.447-1 1-1s1 .448 1 1v2zm1 11.729l.855-.791c1 .484 1.635.852 2.76 1.654 2.113-2.399 3.511-3.616 6.106-5.231l.279.64c-2.141 1.869-3.709 3.949-5.967 7.999-1.393-1.64-2.322-2.686-4.033-4.271z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -46,14 +46,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-certificate-15.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"compass-7-icon\" d=\"M256,90c91.74,0,166,74.243,166,166c0,91.741-74.245,166-166,166c-91.741,0-166-74.245-166-166\r\n\tC90,164.259,164.244,90,256,90 M256,50C142.229,50,50,142.229,50,256s92.229,206,206,206s206-92.229,206-206S369.771,50,256,50z\r\n\t M197.686,216.466l-28.355-47.135l47.225,28.408C209.145,202.733,202.736,209.099,197.686,216.466z M296.709,198.612\r\n\tc6.459,4.562,12.119,10.179,16.729,16.602l29.232-45.883L296.709,198.612z M198.312,297.179l-28.982,45.492l45.416-28.936\r\n\tC208.398,309.163,202.838,303.563,198.312,297.179z M296.018,314.604l46.652,28.066l-28.117-46.74\r\n\tC309.596,303.253,303.299,309.593,296.018,314.604z M400.199,256.001l-99.238,21.998c-4.369,8.913-11.312,16.328-19.859,21.295\r\n\tL256,400.2l-25.104-100.908c-8.545-4.965-15.488-12.381-19.857-21.293l-99.238-21.998l99.238-21.999\r\n\tc4.369-8.913,11.312-16.328,19.857-21.294L256,111.8l25.104,100.908c8.545,4.966,15.488,12.381,19.857,21.294L400.199,256.001z\r\n\t M278.406,256c0-12.374-10.031-22.407-22.406-22.407S233.592,243.626,233.592,256c0,12.376,10.033,22.408,22.408,22.408\r\n\tS278.406,268.376,278.406,256z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M18.625 19.46c-.264 1.696-.97 3.247-2.1 4.54-.065-.461-.254-.908-.433-1.266-.409.216-.899.33-1.328.33l-.265-.016c1.199-1.25 1.791-2.544 1.965-4.27.281.079.623.12 1.053.12.415.284.578.46 1.108.562zm4.875 3.589c-1.197-1.248-1.782-2.549-1.957-4.271-.283.079-.625.122-1.061.122-.414.285-.587.461-1.106.561.264 1.697.97 3.247 2.099 4.54.065-.461.254-.908.433-1.266.51.269 1.131.372 1.592.314zm-4.992-4.829c-.704-.494-.552-.447-1.423-.444h-.002c-.362 0-.685-.225-.794-.557-.267-.797-.176-.673-.879-1.163-.302-.208-.418-.577-.307-.9.273-.793.273-.641 0-1.438-.109-.32.002-.691.307-.9.703-.488.611-.365.879-1.163.109-.333.432-.557.794-.557h.002c.87.002.718.052 1.423-.444.146-.102.318-.154.492-.154s.346.052.492.154c.704.495.552.447 1.423.444h.001c.363 0 .686.224.797.557.266.796.172.673.877 1.163.223.153.348.397.348.65l-.042.25c-.272.793-.272.641 0 1.438.111.317 0 .69-.306.9-.705.489-.611.366-.877 1.163-.205.614-.656.555-1.18.555-.463 0-.465.042-1.041.446-.293.207-.691.207-.984 0zm.492-1.814c1.088 0 1.969-.882 1.969-1.969 0-1.087-.881-1.969-1.969-1.969s-1.969.881-1.969 1.969c0 1.087.881 1.969 1.969 1.969zm-4.674 1.333c-1.675 1.058-3.561 2.247-3.952 2.493-.043-.772-.329-1.492-.828-2.084-.058-.074-5.813-7.4-7.222-9.204-1.109-1.42-.06-2.934 1.23-2.934 1.694 0 2.369 2.207.894 3.163l1.163 1.486 8.053-4.551c1.396-1.032 1.79-2.938.914-4.434-.605-1.032-1.726-1.674-2.924-1.674-.475 0-.936.098-1.373.292l-8.264 4.227c-1.301.624-2.017 1.846-2.017 3.147 0 .816.282 1.663.877 2.412 1.444 1.815 7.261 9.253 7.319 9.328.862 1.147.017 2.753-1.376 2.753-.989 0-1.516-.705-1.667-1.308-.176-.705.069-1.407.773-1.858l-1.141-1.458c-.855.602-1.4 1.515-1.507 2.536-.228 2.174 1.504 3.929 3.557 3.929.63 0 1.255-.174 1.807-.502l5.485-3.458c.264-.415.529-1.431.199-2.301zm-3.319-15.755c.203-.095.431-.145.659-.145.577 0 1.082.305 1.349.816.338.645.244 1.59-.594 2.071l-5.307 3.006c0-1.191-.581-2.284-1.57-2.952l5.463-2.796zm1.987 6.267l1.725 2.117c.348-.525.858-.921 1.46-1.121l-1.562-1.916-1.623.92zm-2.886 8.043l2.871-1.628-.633-.825-2.863 1.661.625.792zm1.842-6.987l-5.012 2.943.624.792 5.026-2.951-.638-.784zm1.286 1.597l-5.035 2.965.624.792 5.049-2.974-.638-.783z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -70,14 +70,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-compass-7.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"download-12-icon\" d=\"M462,246.575c0,44.318-35.928,80.246-80.246,80.246H331.58v-38.119\n\n\tc-0.998-43.379,40.92-44.379,59.67-46.379c-27.168-33.334-70.918-48.244-104.611-48.244c-66.546,0-108.17,39.104-108.17,104.808\n\n\tv27.935h-48.223C85.928,326.821,50,290.894,50,246.575c0-40.982,30.729-74.766,70.396-79.623\n\n\tc2.891-42.287,49.035-66.355,85.217-45.898c19.236-30.605,53.297-50.953,92.115-50.953c57.107,0,103.932,44.033,108.375,100\n\n\tC438.516,180.413,462,210.747,462,246.575z M301.58,288.702c0-30.761,6.053-48.484,31.926-56.837\n\n\tc-20.066-8.452-125.037-28.815-125.037,67.021c0,32.187,0,58.909,0,58.909h-37.408l83.963,84.104l83.965-84.104H301.58\n\n\tC301.58,357.796,301.58,315.59,301.58,288.702z\"/>\n\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1.608 9.476l-1.608-5.476-1.611 5.477c-.429.275-.775.658-1.019 1.107l-5.37 1.416 5.37 1.416c.243.449.589.833 1.019 1.107l1.611 5.477 1.618-5.479c.428-.275.771-.659 1.014-1.109l5.368-1.412-5.368-1.413c-.244-.452-.592-.836-1.024-1.111zm-1.608 4.024c-.828 0-1.5-.672-1.5-1.5s.672-1.5 1.5-1.5 1.5.672 1.5 1.5-.672 1.5-1.5 1.5zm5.25 3.75l-2.573-1.639c.356-.264.67-.579.935-.934l1.638 2.573zm-2.641-8.911l2.64-1.588-1.588 2.639c-.29-.407-.645-.761-1.052-1.051zm-5.215 7.325l-2.644 1.586 1.589-2.641c.29.408.646.764 1.055 1.055zm-1.005-6.34l-1.638-2.573 2.573 1.638c-.357.264-.672.579-.935.935z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -94,14 +94,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-download-12.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"email-2-icon\" d=\"M49.744,103.407v305.186H50.1h411.156h1V103.407H49.744z M415.533,138.407L255.947,260.465\n\n\tL96.473,138.407H415.533z M84.744,173.506l85.504,65.441L84.744,324.45V173.506z M85.1,373.593l113.186-113.186l57.654,44.127\n\n\tl57.375-43.882l112.941,112.94H85.1z M427.256,325.097l-85.896-85.896l85.896-65.695V325.097z\"/>\n\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M6 13h4v-7h4v7h4l-6 6-6-6zm16-1c0 5.514-4.486 10-10 10s-10-4.486-10-10 4.486-10 10-10 10 4.486 10 10zm2 0c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12 12-5.373 12-12z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -118,14 +118,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-email-2.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"error-4-icon\" d=\"M324.76,90L422,187.24v137.52L324.76,422H187.24L90,324.76V187.24L187.24,90H324.76 M341.328,50H170.672\n\tL50,170.672v170.656L170.672,462h170.656L462,341.328V170.672L341.328,50L341.328,50z M228.55,135.812h54.9v166.5h-54.9V135.812z\n\t M256,388.188c-16.362,0-29.625-13.264-29.625-29.625c0-16.362,13.263-29.627,29.625-29.627c16.361,0,29.625,13.265,29.625,29.627\n\tC285.625,374.924,272.361,388.188,256,388.188z\"/>\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 3v18h24v-18h-24zm6.623 7.929l-4.623 5.712v-9.458l4.623 3.746zm-4.141-5.929h19.035l-9.517 7.713-9.518-7.713zm5.694 7.188l3.824 3.099 3.83-3.104 5.612 6.817h-18.779l5.513-6.812zm9.208-1.264l4.616-3.741v9.348l-4.616-5.607z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -142,14 +142,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-error-4.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"flag-3-icon\" d=\"M120.204,462H74.085V50h46.119V462z M437.915,80.746c0,0-29.079,25.642-67.324,25.642\r\n\tc-60.271,0-61.627-51.923-131.596-51.923c-37.832,0-73.106,17.577-88.045,30.381c0,12.64,0,216.762,0,216.762\r\n\tc21.204-14.696,53.426-30.144,88.286-30.144c66.08,0,75.343,49.388,134.242,49.388c38.042,0,64.437-24.369,64.437-24.369V80.746z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M16.143 2l5.857 5.858v8.284l-5.857 5.858h-8.286l-5.857-5.858v-8.284l5.857-5.858h8.286zm.828-2h-9.942l-7.029 7.029v9.941l7.029 7.03h9.941l7.03-7.029v-9.942l-7.029-7.029zm-6.471 6h3l-1 8h-1l-1-8zm1.5 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -166,14 +166,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-4-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-flag-3.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"home-4-icon\" d=\"M419.492,275.815v166.213H300.725v-90.33h-89.451v90.33H92.507V275.815H50L256,69.972l206,205.844H419.492\n\n\tz M394.072,88.472h-47.917v38.311l47.917,48.023V88.472z\"/>\n\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M4 24h-2v-24h2v24zm18-21.387s-1.621 1.43-3.754 1.43c-3.36 0-3.436-2.895-7.337-2.895-2.108 0-4.075.98-4.909 1.694v12.085c1.184-.819 2.979-1.681 4.923-1.681 3.684 0 4.201 2.754 7.484 2.754 2.122 0 3.593-1.359 3.593-1.359v-12.028z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -190,14 +190,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-6-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-home-7.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"info-6-icon\" d=\"M256,90.002c91.74,0,166,74.241,166,165.998c0,91.739-74.245,165.998-166,165.998\n\tc-91.738,0-166-74.242-166-165.998C90,164.259,164.243,90.002,256,90.002 M256,50.002C142.229,50.002,50,142.228,50,256\n\tc0,113.769,92.229,205.998,206,205.998c113.77,0,206-92.229,206-205.998C462,142.228,369.77,50.002,256,50.002L256,50.002z\n\t M252.566,371.808c-28.21,9.913-51.466-1.455-46.801-28.547c4.667-27.098,31.436-85.109,35.255-96.079\n\tc3.816-10.97-3.502-13.977-11.346-9.513c-4.524,2.61-11.248,7.841-17.02,12.925c-1.601-3.223-3.852-6.906-5.542-10.433\n\tc9.419-9.439,25.164-22.094,43.803-26.681c22.27-5.497,59.492,3.29,43.494,45.858c-11.424,30.34-19.503,51.276-24.594,66.868\n\tc-5.088,15.598,0.955,18.868,9.863,12.791c6.959-4.751,14.372-11.214,19.806-16.226c2.515,4.086,3.319,5.389,5.806,10.084\n\tC295.857,342.524,271.182,365.151,252.566,371.808z M311.016,184.127c-12.795,10.891-31.76,10.655-42.37-0.532\n\tc-10.607-11.181-8.837-29.076,3.955-39.969c12.794-10.89,31.763-10.654,42.37,0.525\n\tC325.577,155.337,323.809,173.231,311.016,184.127z\"/>\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M20 7.093v-5.093h-3v2.093l3 3zm4 5.907l-12-12-12 12h3v10h7v-5h4v5h7v-10h3zm-5 8h-3v-5h-8v5h-3v-10.26l7-6.912 7 6.99v10.182z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -214,14 +214,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-2-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-info-8.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"key-2-icon\" stroke=\"#000000\" stroke-miterlimit=\"10\" d=\"M286.529,325.486l-45.314,45.314h-43.873l0.002,43.872\r\n\tl-45.746-0.001v41.345l-100.004-0.001l150.078-150.076c-4.578-4.686-10.061-11.391-13.691-17.423L50,426.498v-40.939\r\n\tl145.736-145.736C212.174,278.996,244.713,310.705,286.529,325.486z M425.646,92.339c48.473,48.473,48.471,127.064-0.002,175.535\r\n\tc-48.477,48.476-127.061,48.476-175.537,0.001c-48.473-48.472-48.475-127.062,0-175.537\r\n\tC298.58,43.865,377.172,43.865,425.646,92.339z M400.73,117.165c-12.023-12.021-31.516-12.021-43.537,0\r\n\tc-12.021,12.022-12.021,31.517,0,43.538s31.514,12.021,43.537-0.001C412.754,148.68,412.75,129.188,400.73,117.165z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-2.033 16.01c.564-1.789 1.632-3.932 1.821-4.474.273-.787-.211-1.136-1.74.209l-.34-.64c1.744-1.897 5.335-2.326 4.113.613-.763 1.835-1.309 3.074-1.621 4.03-.455 1.393.694.828 1.819-.211.153.25.203.331.356.619-2.498 2.378-5.271 2.588-4.408-.146zm4.742-8.169c-.532.453-1.32.443-1.761-.022-.441-.465-.367-1.208.164-1.661.532-.453 1.32-.442 1.761.022.439.466.367 1.209-.164 1.661z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -238,14 +238,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-lock-3-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-key-3.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"lock-3-icon\" d=\"M195.334,223.333h-50v-62.666C145.334,99.645,194.979,50,256,50c61.022,0,110.667,49.645,110.667,110.667\r\n\tv62.666h-50v-62.666C316.667,127.215,289.452,100,256,100c-33.451,0-60.666,27.215-60.666,60.667V223.333z M404,253.333V462H108\r\n\tV253.333H404z M283,341c0-14.912-12.088-27-27-27s-27,12.088-27,27c0,7.811,3.317,14.844,8.619,19.773\r\n\tc4.385,4.075,6.881,9.8,6.881,15.785V399.5h23v-22.941c0-5.989,2.494-11.708,6.881-15.785C279.683,355.844,283,348.811,283,341z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12.451 17.337l-2.451 2.663h-2v2h-2v2h-6v-1.293l7.06-7.06c-.214-.26-.413-.533-.599-.815l-6.461 6.461v-2.293l6.865-6.949c1.08 2.424 3.095 4.336 5.586 5.286zm11.549-9.337c0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8 8 3.582 8 8zm-3-3c0-1.104-.896-2-2-2s-2 .896-2 2 .896 2 2 2 2-.896 2-2z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -262,14 +262,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-magnifier-4.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"magnifier-4-icon\" d=\"M448.225,394.243l-85.387-85.385c16.55-26.081,26.146-56.986,26.146-90.094\n\n\tc0-92.989-75.652-168.641-168.643-168.641c-92.989,0-168.641,75.652-168.641,168.641s75.651,168.641,168.641,168.641\n\n\tc31.465,0,60.939-8.67,86.175-23.735l86.14,86.142C429.411,486.566,485.011,431.029,448.225,394.243z M103.992,218.764\n\n\tc0-64.156,52.192-116.352,116.35-116.352s116.353,52.195,116.353,116.352s-52.195,116.352-116.353,116.352\n\n\tS103.992,282.92,103.992,218.764z M138.455,188.504c34.057-78.9,148.668-69.752,170.248,12.862\n\n\tC265.221,150.329,188.719,144.834,138.455,188.504z\"/>\n\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M23.111 20.058l-4.977-4.977c.965-1.52 1.523-3.322 1.523-5.251 0-5.42-4.409-9.83-9.829-9.83-5.42 0-9.828 4.41-9.828 9.83s4.408 9.83 9.829 9.83c1.834 0 3.552-.505 5.022-1.383l5.021 5.021c2.144 2.141 5.384-1.096 3.239-3.24zm-20.064-10.228c0-3.739 3.043-6.782 6.782-6.782s6.782 3.042 6.782 6.782-3.043 6.782-6.782 6.782-6.782-3.043-6.782-6.782zm2.01-1.764c1.984-4.599 8.664-4.066 9.922.749-2.534-2.974-6.993-3.294-9.922-.749z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -286,14 +286,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-6-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-mobile-phone-7.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"mobile-phone-6-icon\" d=\"M139.59,131.775c-13.807,0-25,11.197-25,25.01V436.99c0,13.812,11.193,25.01,25,25.01h150.49\r\n\tc13.807,0,25-11.198,25-25.01V156.766c0-13.802-11.186-24.99-24.98-24.99H139.59z M179.832,416.514h-30.996v-24.51h30.996V416.514z\r\n\t M179.832,372.203h-30.996v-24.51h30.996V372.203z M230.334,416.514h-30.996v-24.51h30.996V416.514z M230.334,372.203h-30.996\r\n\tv-24.51h30.996V372.203z M280.836,416.514H249.84v-24.51h30.996V416.514z M280.836,372.203H249.84v-24.51h30.996V372.203z\r\n\t M280.836,312.887h-132V183.226h132V312.887z M283.451,111.408c13.445-0.01,26.9,5.113,37.164,15.369s15.4,23.699,15.41,37.147\r\n\th22.121c-0.012-19.113-7.312-38.231-21.898-52.805c-14.588-14.573-33.691-21.854-52.797-21.842V111.408z M283.451,72.682\r\n\tc23.354-0.015,46.691,8.882,64.52,26.696c17.828,17.812,26.75,41.187,26.766,64.547h22.674c-0.02-29.166-11.16-58.358-33.418-80.597\r\n\tC341.734,61.089,312.605,49.982,283.451,50V72.682z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M5 6c-1.104 0-2 .896-2 2v14c0 1.104.896 2 2 2h8c1.104 0 2-.896 2-2v-14c0-1.104-.896-2-2-2h-8zm2 15h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm3 2h-2v-1h2v1zm0-2h-2v-1h2v1zm0-3h-8v-7h8v7zm0-11.688c.944-.001 1.889.359 2.608 1.08.721.72 1.082 1.664 1.082 2.606h1.554c-.001-1.341-.514-2.684-1.538-3.707-1.025-1.022-2.365-1.533-3.706-1.532v1.553zm0-2.718c1.639-.001 3.277.623 4.53 1.874 1.251 1.25 1.877 2.892 1.878 4.531h1.592c-.001-2.047-.782-4.096-2.345-5.658-1.562-1.562-3.609-2.341-5.655-2.34v1.593z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -310,14 +310,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-10-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-pen-14.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- License Agreement at http://iconmonstr.com/license/ -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"512px\" height=\"512px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"pen-10-icon\" d=\"M244.558,199.493l67.827,67.826l-73.17,134.531c0,0-90.805,23.4-147.694,60.027l-14.185-14.182\r\n\tl68.113-68.105c5.975-5.982,13.726-9.773,22.11-10.807c4.642-0.582,9.128-2.621,12.696-6.205c8.538-8.547,8.546-22.4-0.002-30.951\r\n\tc-8.549-8.543-22.407-8.543-30.959-0.002c-3.573,3.572-5.623,8.061-6.199,12.693c-1.028,8.371-4.834,16.15-10.8,22.117\r\n\tl-68.104,68.105L50,420.354c37.028-57.496,60.021-147.693,60.021-147.693L244.558,199.493z M315.896,50.122\r\n\tc-22.784,44.143-53.014,100-53.014,100l98.872,98.869c0,0,55.909-30.086,100.246-52.766L315.896,50.122z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12.014 6.54s2.147-3.969 3.475-6.54l8.511 8.511c-2.583 1.321-6.556 3.459-6.556 3.459l-5.43-5.43zm-8.517 6.423s-1.339 5.254-3.497 8.604l.827.826 3.967-3.967c.348-.348.569-.801.629-1.288.034-.27.153-.532.361-.74.498-.498 1.306-.498 1.803 0 .498.499.498 1.305 0 1.803-.208.209-.469.328-.74.361-.488.061-.94.281-1.288.63l-3.967 3.968.826.84c3.314-2.133 8.604-3.511 8.604-3.511l4.262-7.837-3.951-3.951-7.836 4.262z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -334,14 +334,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-2-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-tag-3.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"tag-2-icon\" d=\"M234.508,50L50.068,50.262l-0.004,184.311L277.365,462l184.57-184.57L234.508,50z M114.877,167.365\n\n\tc-15.027-15.027-15.027-39.395,0-54.424c15.029-15.029,39.396-15.029,54.426,0s15.029,39.396,0,54.424\n\n\tC154.273,182.395,129.906,182.395,114.877,167.365z M242.316,327.94l-76.225-76.226l17.678-17.678l76.225,76.226L242.316,327.94z\n\n\t M317.609,335.887L199.764,218.041l17.678-17.678l117.846,117.846L317.609,335.887z M351.818,301.678L233.973,183.832l17.678-17.678\n\n\tL369.496,284L351.818,301.678z\"/>\n\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M10.605 0h-10.604v10.609l13.39 13.391 10.609-10.605-13.395-13.395zm-7.019 6.414c-.781-.782-.781-2.047 0-2.828.782-.781 2.048-.781 2.828-.002.782.783.782 2.048 0 2.83-.781.781-2.046.781-2.828 0zm6.823 8.947l-4.243-4.242.708-.708 4.243 4.243-.708.707zm4.949.707l-7.07-7.071.707-.707 7.071 7.071-.708.707zm2.121-2.121l-7.071-7.071.707-.707 7.071 7.071-.707.707z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -358,14 +358,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-time-13-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-user-5.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"time-13-icon\" d=\"M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933\r\n\tc0,25.044,8.566,49.646,24.121,69.273l50.056,63.166c9.206,11.617,9.271,27.895,0.159,39.584l-50.768,65.13\r\n\tc-15.198,19.497-23.568,43.85-23.568,68.571V462h259.5v-53.343c0-24.722-8.37-49.073-23.567-68.571l-50.769-65.13\r\n\tc-9.112-11.689-9.047-27.967,0.159-39.584L361.629,172.206z M330.634,364.678c11.412,14.64,15.116,29.947,15.116,47.321h-11.096\r\n\tc-4.586-17.886-31.131-30.642-62.559-47.586c-6.907-3.724-6.096-10.373-6.096-15.205h-20c0,4.18,1.03,11.365-6.106,15.202\r\n\tc-32.073,17.249-58.274,29.705-62.701,47.589H166.25c0-17.261,3.645-32.605,15.115-47.321l50.769-65.13\r\n\tc7.109-9.12,11.723-19.484,13.866-30.22v13.38h20V269.33c2.144,10.734,6.758,21.098,13.866,30.218L330.634,364.678z\r\n\t M197.966,167.862l-16.245-20.5c-11.538-14.56-15.471-30.096-15.471-47.361h179.5c0,17.149-3.872,32.727-15.471,47.361l-16.245,20.5\r\n\tH197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z\"/>\r\n</svg>\r\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M19 7.001c0 3.865-3.134 7-7 7s-7-3.135-7-7c0-3.867 3.134-7.001 7-7.001s7 3.134 7 7.001zm-1.598 7.18c-1.506 1.137-3.374 1.82-5.402 1.82-2.03 0-3.899-.685-5.407-1.822-4.072 1.793-6.593 7.376-6.593 9.821h24c0-2.423-2.6-8.006-6.598-9.819z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -382,14 +382,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-6-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-warning-8.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n<path id=\"warning-6-icon\" d=\"M239.939,231.352h32.121v97.421h-32.121V231.352z M256,379.019c-9.574,0-17.334-7.761-17.334-17.334\n\tc0-9.574,7.76-17.335,17.334-17.335c9.573,0,17.334,7.761,17.334,17.335C273.334,371.258,265.573,379.019,256,379.019z M256,78.07\n\tL50,434.873h412L256,78.07z M256,158.07l136.718,236.803H119.282L256,158.07z\"/>\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M12 5.177l8.631 15.823h-17.262l8.631-15.823zm0-4.177l-12 22h24l-12-22zm-1 9h2v6h-2v-6zm1 9.75c-.689 0-1.25-.56-1.25-1.25s.561-1.25 1.25-1.25 1.25.56 1.25 1.25-.561 1.25-1.25 1.25z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -406,38 +406,14 @@ root: root
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-wireless-6-icon.svg"] = (function() {
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-8.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n\r\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \r\nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\r\n\r\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\r\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\r\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\r\n<path id=\"wireless-6-icon\" d=\"M50,178.599c52.72-52.72,125.552-85.328,206-85.328c80.448,0,153.28,32.608,206,85.328l-35,35\r\n\tc-43.763-43.763-104.221-70.83-171-70.83c-66.78,0-127.237,27.067-171,70.83L50,178.599z M148.196,276.796\r\n\tc27.589-27.59,65.704-44.654,107.804-44.654s80.215,17.064,107.804,44.654l35.935-35.936\r\n\tc-36.785-36.787-87.604-59.539-143.738-59.539s-106.953,22.752-143.738,59.539L148.196,276.796z M211,339.599\r\n\tc11.517-11.517,27.427-18.64,45-18.64s33.483,7.123,45,18.64l35.313-35.312c-20.554-20.554-48.949-33.269-80.313-33.269\r\n\ts-59.76,12.715-80.313,33.269L211,339.599z M256,356.138c-17.284,0-31.299,14.01-31.299,31.297\r\n\tc0,17.285,14.015,31.295,31.299,31.295c17.283,0,31.296-14.01,31.296-31.295C287.296,370.147,273.283,356.138,256,356.138z\"/>\r\n</svg>\r\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
cb(null, output);
|
||||
}
|
||||
;
|
||||
} catch (e) {
|
||||
cb(runtime.handleError(e, lineno, colno));
|
||||
}
|
||||
}
|
||||
return {
|
||||
root: root
|
||||
};
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["img/iconmonstr-x-mark-5-icon.svg"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated. \nYou may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->\n\n\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n\n<svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\"\n\n\t width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n\n<path id=\"x-mark-5-icon\" d=\"M432.546,133.462L367.133,76.39L254.078,210.715L140.967,73.702l-61.513,65.068\n\n\tc33.791,43.885,78.146,89.797,123.688,132.465L82.993,413.987l19.865,22.629c29.251-20.31,87.839-65.578,150.312-120.092\n\n\tc63.662,55.812,122.861,101.336,151.301,121.773l21.438-19.443L303.804,270.95C352.439,225.709,399.308,177.442,432.546,133.462z\"/>\n\n</svg>\n\n";
|
||||
output += "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 3.752l-4.423-3.752-7.771 9.039-7.647-9.008-4.159 4.278c2.285 2.885 5.284 5.903 8.362 8.708l-8.165 9.447 1.343 1.487c1.978-1.335 5.981-4.373 10.205-7.958 4.304 3.67 8.306 6.663 10.229 8.006l1.449-1.278-8.254-9.724c3.287-2.973 6.584-6.354 8.831-9.245z\"/></svg>";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -461,7 +437,7 @@ var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n <title>Certidude server</title>\n <link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\"/>\n <script type=\"text/javascript\" src=\"/js/jquery-2.1.4.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/nunjucks-slim.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/templates.js\"></script>\n <script type=\"text/javascript\" src=\"/js/certidude.js\"></script>\n <link rel=\"shortcut icon\" href=\"data:image/x-icon;,\" type=\"image/x-icon\">\n</head>\n<body>\n <nav id=\"menu\">\n <ul class=\"container\">\n <li data-section=\"requests\">Requests</li>\n <li data-section=\"signed\">Signed</li>\n <li data-section=\"revoked\">Revoked</li>\n <li data-section=\"config\">Configuration</li>\n <li data-section=\"log\">Log</li>\n </ul>\n </nav>\n <div id=\"container\" class=\"container\">\n Loading certificate authority...\n </div>\n</body>\n\n<footer>\n <a href=\"http://github.com/laurivosandi/certidude\">Certidude</a> by\n <a href=\"http://github.com/laurivosandi/\">Lauri Võsandi</a>\n</footer>\n\n</html>\n\n";
|
||||
output += "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n <title>Certidude server</title>\n <link href=\"/css/style.css\" rel=\"stylesheet\" type=\"text/css\"/>\n <script type=\"text/javascript\" src=\"/js/jquery-2.1.4.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/nunjucks-slim.min.js\"></script>\n <script type=\"text/javascript\" src=\"/js/templates.js\"></script>\n <script type=\"text/javascript\" src=\"/js/certidude.js\"></script>\n <link rel=\"shortcut icon\" href=\"data:image/x-icon;,\" type=\"image/x-icon\">\n</head>\n<body>\n <nav id=\"menu\">\n <ul class=\"container\">\n <li data-section=\"about\">Profile</li>\n <li id=\"section-requests\" data-section=\"requests\" style=\"display:none;\">Requests</li>\n <li id=\"section-signed\" data-section=\"signed\" style=\"display:none;\">Signed</li>\n <li id=\"section-revoked\" data-section=\"revoked\" style=\"display:none;\">Revoked</li>\n <li id=\"section-config\" data-section=\"config\" style=\"display:none;\">Configuration</li>\n <li id=\"section-log\" data-section=\"log\" style=\"display:none;\">Log</li>\n </ul>\n </nav>\n <div id=\"container\" class=\"container\">\n Loading certificate authority...\n </div>\n</body>\n\n<footer>\n <a href=\"http://github.com/laurivosandi/certidude\">Certidude</a> by\n <a href=\"http://github.com/laurivosandi/\">Lauri Võsandi</a>\n</footer>\n\n</html>\n\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
@ -485,12 +461,38 @@ var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "\n<section id=\"about\">\n<p>Hi ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"username"), env.opts.autoescape);
|
||||
output += ",</p>\n\n<p>Request submission is allowed from: ";
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets")) {
|
||||
output += "\n<section id=\"about\">\n<h2>";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"gn"), env.opts.autoescape);
|
||||
output += " ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"sn"), env.opts.autoescape);
|
||||
output += " (";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"name"), env.opts.autoescape);
|
||||
output += ") settings</h2>\n\n<p>Mails will be sent to: ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user")),"mail"), env.opts.autoescape);
|
||||
output += "</p>\n\n<p>You can click <a href=\"/api/bundle/\">here</a> to generate bundle\nfor current user account.</p>\n\n";
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) {
|
||||
output += "\n\n<h2>Authority certificate</h2>\n\n<p>Several things such as CRL location and e-mails are hardcoded into\nthe <a href=\"/api/certificate\">certificate</a> and\nas such require complete reset of X509 infrastructure if some of them needs to be changed:</p>\n\n<p>Mails will appear from: ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"certificate")),"email_address"), env.opts.autoescape);
|
||||
output += "</p>\n\n\n<h2>Authority settings</h2>\n\n<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>\n\n<p>Outgoing mail server:\n";
|
||||
if(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox")) {
|
||||
output += "\n ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"outbox"), env.opts.autoescape);
|
||||
output += "\n";
|
||||
;
|
||||
}
|
||||
else {
|
||||
output += "\n E-mail disabled\n";
|
||||
;
|
||||
}
|
||||
output += "</p>\n\n<p>Authenticated users allowed from:\n\n";
|
||||
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) {
|
||||
output += "\n anywhere\n </p>\n";
|
||||
;
|
||||
}
|
||||
else {
|
||||
output += "\n </p>\n <ul>\n ";
|
||||
frame = frame.push();
|
||||
var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets");
|
||||
var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets");
|
||||
if(t_3) {var t_2 = t_3.length;
|
||||
for(var t_1=0; t_1 < t_3.length; t_1++) {
|
||||
var t_4 = t_3[t_1];
|
||||
@ -502,26 +504,29 @@ frame.set("loop.revindex0", t_2 - t_1 - 1);
|
||||
frame.set("loop.first", t_1 === 0);
|
||||
frame.set("loop.last", t_1 === t_2 - 1);
|
||||
frame.set("loop.length", t_2);
|
||||
output += "\n <li>";
|
||||
output += runtime.suppressValue(t_4, env.opts.autoescape);
|
||||
output += " ";
|
||||
output += "</li>\n ";
|
||||
;
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n </ul>\n";
|
||||
;
|
||||
}
|
||||
output += "\n\n\n<p>Request submission is allowed from:\n\n";
|
||||
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) {
|
||||
output += "\n anywhere\n </p>\n";
|
||||
;
|
||||
}
|
||||
else {
|
||||
output += "anywhere";
|
||||
;
|
||||
}
|
||||
output += "</p>\n<p>Autosign is allowed from: ";
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets")) {
|
||||
output += "\n </p>\n <ul>\n ";
|
||||
frame = frame.push();
|
||||
var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets");
|
||||
var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets");
|
||||
if(t_7) {var t_6 = t_7.length;
|
||||
for(var t_5=0; t_5 < t_7.length; t_5++) {
|
||||
var t_8 = t_7[t_5];
|
||||
frame.set("i", t_8);
|
||||
frame.set("subnet", t_8);
|
||||
frame.set("loop.index", t_5 + 1);
|
||||
frame.set("loop.index0", t_5);
|
||||
frame.set("loop.revindex", t_6 - t_5);
|
||||
@ -529,26 +534,29 @@ frame.set("loop.revindex0", t_6 - t_5 - 1);
|
||||
frame.set("loop.first", t_5 === 0);
|
||||
frame.set("loop.last", t_5 === t_6 - 1);
|
||||
frame.set("loop.length", t_6);
|
||||
output += "\n <li>";
|
||||
output += runtime.suppressValue(t_8, env.opts.autoescape);
|
||||
output += " ";
|
||||
output += "</li>\n ";
|
||||
;
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n </ul>\n";
|
||||
;
|
||||
}
|
||||
output += "\n\n<p>Autosign is allowed from:\n";
|
||||
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) {
|
||||
output += "\n anywhere\n </p>\n";
|
||||
;
|
||||
}
|
||||
else {
|
||||
output += "nowhere";
|
||||
;
|
||||
}
|
||||
output += "</p>\n<p>Authority administration is allowed from: ";
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets")) {
|
||||
output += "\n </p>\n <ul>\n ";
|
||||
frame = frame.push();
|
||||
var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets");
|
||||
var t_11 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets");
|
||||
if(t_11) {var t_10 = t_11.length;
|
||||
for(var t_9=0; t_9 < t_11.length; t_9++) {
|
||||
var t_12 = t_11[t_9];
|
||||
frame.set("i", t_12);
|
||||
frame.set("subnet", t_12);
|
||||
frame.set("loop.index", t_9 + 1);
|
||||
frame.set("loop.index0", t_9);
|
||||
frame.set("loop.revindex", t_10 - t_9);
|
||||
@ -556,25 +564,29 @@ frame.set("loop.revindex0", t_10 - t_9 - 1);
|
||||
frame.set("loop.first", t_9 === 0);
|
||||
frame.set("loop.last", t_9 === t_10 - 1);
|
||||
frame.set("loop.length", t_10);
|
||||
output += "\n <li>";
|
||||
output += runtime.suppressValue(t_12, env.opts.autoescape);
|
||||
output += " ";
|
||||
output += "</li>\n ";
|
||||
;
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n </ul>\n";
|
||||
;
|
||||
}
|
||||
output += "\n\n<p>Authority administration is allowed from:\n";
|
||||
if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) {
|
||||
output += "\n anywhere\n </p>\n";
|
||||
;
|
||||
}
|
||||
else {
|
||||
output += "anywhere";
|
||||
;
|
||||
}
|
||||
output += "\n<p>Authority administration allowed for: ";
|
||||
output += "\n <ul>\n ";
|
||||
frame = frame.push();
|
||||
var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users");
|
||||
var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets");
|
||||
if(t_15) {var t_14 = t_15.length;
|
||||
for(var t_13=0; t_13 < t_15.length; t_13++) {
|
||||
var t_16 = t_15[t_13];
|
||||
frame.set("i", t_16);
|
||||
frame.set("subnet", t_16);
|
||||
frame.set("loop.index", t_13 + 1);
|
||||
frame.set("loop.index0", t_13);
|
||||
frame.set("loop.revindex", t_14 - t_13);
|
||||
@ -582,71 +594,132 @@ frame.set("loop.revindex0", t_14 - t_13 - 1);
|
||||
frame.set("loop.first", t_13 === 0);
|
||||
frame.set("loop.last", t_13 === t_14 - 1);
|
||||
frame.set("loop.length", t_14);
|
||||
output += "\n <li>";
|
||||
output += runtime.suppressValue(t_16, env.opts.autoescape);
|
||||
output += " ";
|
||||
output += "</li>\n ";
|
||||
;
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "</p>\n</section>\n";
|
||||
var t_17;
|
||||
t_17 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
|
||||
frame.set("s", t_17, true);
|
||||
if(frame.topLevel) {
|
||||
context.setVariable("s", t_17);
|
||||
output += "\n </ul>\n";
|
||||
;
|
||||
}
|
||||
if(frame.topLevel) {
|
||||
context.addExport("s", t_17);
|
||||
}
|
||||
output += "\n\n\n<section id=\"requests\">\n <h1>Pending requests</h1>\n\n\n <ul id=\"pending_requests\">\n ";
|
||||
output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n";
|
||||
frame = frame.push();
|
||||
var t_20 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"requests");
|
||||
runtime.asyncEach(t_20, 1, function(request, t_18, t_19,next) {
|
||||
frame.set("request", request);
|
||||
frame.set("loop.index", t_18 + 1);
|
||||
frame.set("loop.index0", t_18);
|
||||
frame.set("loop.revindex", t_19 - t_18);
|
||||
frame.set("loop.revindex0", t_19 - t_18 - 1);
|
||||
frame.set("loop.first", t_18 === 0);
|
||||
frame.set("loop.last", t_18 === t_19 - 1);
|
||||
frame.set("loop.length", t_19);
|
||||
output += "\n ";
|
||||
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_23,t_21) {
|
||||
if(t_23) { cb(t_23); return; }
|
||||
t_21.render(context.getVariables(), frame, function(t_24,t_22) {
|
||||
if(t_24) { cb(t_24); return; }
|
||||
output += t_22
|
||||
output += "\n\t ";
|
||||
next(t_18);
|
||||
})});
|
||||
}, function(t_26,t_25) {
|
||||
if(t_26) { cb(t_26); return; }
|
||||
var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users");
|
||||
if(t_19) {var t_17;
|
||||
if(runtime.isArray(t_19)) {
|
||||
var t_18 = t_19.length;
|
||||
for(t_17=0; t_17 < t_19.length; t_17++) {
|
||||
var t_20 = t_19[t_17][0]
|
||||
frame.set("handle", t_19[t_17][0]);
|
||||
var t_21 = t_19[t_17][1]
|
||||
frame.set("full_name", t_19[t_17][1]);
|
||||
frame.set("loop.index", t_17 + 1);
|
||||
frame.set("loop.index0", t_17);
|
||||
frame.set("loop.revindex", t_18 - t_17);
|
||||
frame.set("loop.revindex0", t_18 - t_17 - 1);
|
||||
frame.set("loop.first", t_17 === 0);
|
||||
frame.set("loop.last", t_17 === t_18 - 1);
|
||||
frame.set("loop.length", t_18);
|
||||
output += "\n <li>";
|
||||
output += runtime.suppressValue(t_21, env.opts.autoescape);
|
||||
output += "</li>\n";
|
||||
;
|
||||
}
|
||||
} else {
|
||||
t_17 = -1;
|
||||
var t_18 = runtime.keys(t_19).length;
|
||||
for(var t_22 in t_19) {
|
||||
t_17++;
|
||||
var t_23 = t_19[t_22];
|
||||
frame.set("handle", t_22);
|
||||
frame.set("full_name", t_23);
|
||||
frame.set("loop.index", t_17 + 1);
|
||||
frame.set("loop.index0", t_17);
|
||||
frame.set("loop.revindex", t_18 - t_17);
|
||||
frame.set("loop.revindex0", t_18 - t_17 - 1);
|
||||
frame.set("loop.first", t_17 === 0);
|
||||
frame.set("loop.last", t_17 === t_18 - 1);
|
||||
frame.set("loop.length", t_18);
|
||||
output += "\n <li>";
|
||||
output += runtime.suppressValue(t_23, env.opts.autoescape);
|
||||
output += "</li>\n";
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n <li class=\"notify\">\n <p>No certificate signing requests to sign! You can submit a certificate signing request by:</p>\n <pre>certidude setup client ";
|
||||
output += "\n</ul>\n</section>\n\n";
|
||||
;
|
||||
}
|
||||
else {
|
||||
output += "\n<p>Here you can renew your certificates</p>\n\n";
|
||||
;
|
||||
}
|
||||
output += "\n\n";
|
||||
var t_24;
|
||||
t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
|
||||
frame.set("s", t_24, true);
|
||||
if(frame.topLevel) {
|
||||
context.setVariable("s", t_24);
|
||||
}
|
||||
if(frame.topLevel) {
|
||||
context.addExport("s", t_24);
|
||||
}
|
||||
output += "\n\n\n";
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) {
|
||||
output += "\n<section id=\"requests\">\n <h1>Pending requests</h1>\n\n <p>Submit a certificate signing request with Certidude:</p>\n <pre>certidude setup client ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
|
||||
output += "</pre>\n </li>\n </ul>\n</section>\n\n\n<section id=\"signed\">\n <h1>Signed certificates</h1>\n <input id=\"search\" type=\"search\" class=\"icon search\">\n <ul id=\"signed_certificates\">\n ";
|
||||
output += "</pre>\n\n <ul id=\"pending_requests\">\n ";
|
||||
frame = frame.push();
|
||||
var t_29 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"signed")));
|
||||
runtime.asyncEach(t_29, 1, function(certificate, t_27, t_28,next) {
|
||||
frame.set("certificate", certificate);
|
||||
frame.set("loop.index", t_27 + 1);
|
||||
frame.set("loop.index0", t_27);
|
||||
frame.set("loop.revindex", t_28 - t_27);
|
||||
frame.set("loop.revindex0", t_28 - t_27 - 1);
|
||||
frame.set("loop.first", t_27 === 0);
|
||||
frame.set("loop.last", t_27 === t_28 - 1);
|
||||
frame.set("loop.length", t_28);
|
||||
output += "\n ";
|
||||
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_32,t_30) {
|
||||
var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
|
||||
if(t_27) {var t_26 = t_27.length;
|
||||
for(var t_25=0; t_25 < t_27.length; t_25++) {
|
||||
var t_28 = t_27[t_25];
|
||||
frame.set("request", t_28);
|
||||
frame.set("loop.index", t_25 + 1);
|
||||
frame.set("loop.index0", t_25);
|
||||
frame.set("loop.revindex", t_26 - t_25);
|
||||
frame.set("loop.revindex0", t_26 - t_25 - 1);
|
||||
frame.set("loop.first", t_25 === 0);
|
||||
frame.set("loop.last", t_25 === t_26 - 1);
|
||||
frame.set("loop.length", t_26);
|
||||
output += "\n ";
|
||||
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) {
|
||||
if(t_31) { cb(t_31); return; }
|
||||
t_29.render(context.getVariables(), frame, function(t_32,t_30) {
|
||||
if(t_32) { cb(t_32); return; }
|
||||
t_30.render(context.getVariables(), frame, function(t_33,t_31) {
|
||||
if(t_33) { cb(t_33); return; }
|
||||
output += t_31
|
||||
output += t_30
|
||||
output += "\n\t ";
|
||||
next(t_27);
|
||||
})});
|
||||
}, function(t_35,t_34) {
|
||||
if(t_35) { cb(t_35); return; }
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n <li class=\"notify\">\n <p>No certificate signing requests to sign!</p>\n </li>\n </ul>\n</section>\n\n<section id=\"signed\">\n <h1>Signed certificates</h1>\n <input id=\"search\" type=\"search\" class=\"icon search\">\n <ul id=\"signed_certificates\">\n ";
|
||||
frame = frame.push();
|
||||
var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed")));
|
||||
if(t_35) {var t_34 = t_35.length;
|
||||
for(var t_33=0; t_33 < t_35.length; t_33++) {
|
||||
var t_36 = t_35[t_33];
|
||||
frame.set("certificate", t_36);
|
||||
frame.set("loop.index", t_33 + 1);
|
||||
frame.set("loop.index0", t_33);
|
||||
frame.set("loop.revindex", t_34 - t_33);
|
||||
frame.set("loop.revindex0", t_34 - t_33 - 1);
|
||||
frame.set("loop.first", t_33 === 0);
|
||||
frame.set("loop.last", t_33 === t_34 - 1);
|
||||
frame.set("loop.length", t_34);
|
||||
output += "\n ";
|
||||
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) {
|
||||
if(t_39) { cb(t_39); return; }
|
||||
t_37.render(context.getVariables(), frame, function(t_40,t_38) {
|
||||
if(t_40) { cb(t_40); return; }
|
||||
output += t_38
|
||||
output += "\n\t ";
|
||||
})});
|
||||
}
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n </ul>\n</section>\n\n<section id=\"log\">\n <h1>Log</h1>\n <p>\n <input id=\"log_level_critical\" type=\"checkbox\" checked/> <label for=\"log_level_critical\">Critical</label>\n <input id=\"log_level_error\" type=\"checkbox\" checked/> <label for=\"log_level_error\">Errors</label>\n <input id=\"log_level_warning\" type=\"checkbox\" checked/> <label for=\"log_level_warning\">Warnings</label>\n <input id=\"log_level_info\" type=\"checkbox\" checked/> <label for=\"log_level_info\">Info</label>\n <input id=\"log_level_debug\" type=\"checkbox\"/> <label for=\"log_level_debug\">Debug</label>\n </p>\n <ul id=\"log_entries\">\n </ul>\n</section>\n\n<section id=\"revoked\">\n <h1>Revoked certificates</h1>\n <p>To fetch certificate revocation list:</p>\n <pre>curl ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "window")),"location")),"href"), env.opts.autoescape);
|
||||
@ -656,41 +729,44 @@ output += "/certificate/ > session.pem\n openssl ocsp -issuer session.pem -CA
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape);
|
||||
output += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n ";
|
||||
frame = frame.push();
|
||||
var t_38 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"revoked");
|
||||
if(t_38) {var t_37 = t_38.length;
|
||||
for(var t_36=0; t_36 < t_38.length; t_36++) {
|
||||
var t_39 = t_38[t_36];
|
||||
frame.set("j", t_39);
|
||||
frame.set("loop.index", t_36 + 1);
|
||||
frame.set("loop.index0", t_36);
|
||||
frame.set("loop.revindex", t_37 - t_36);
|
||||
frame.set("loop.revindex0", t_37 - t_36 - 1);
|
||||
frame.set("loop.first", t_36 === 0);
|
||||
frame.set("loop.last", t_36 === t_37 - 1);
|
||||
frame.set("loop.length", t_37);
|
||||
var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
|
||||
if(t_43) {var t_42 = t_43.length;
|
||||
for(var t_41=0; t_41 < t_43.length; t_41++) {
|
||||
var t_44 = t_43[t_41];
|
||||
frame.set("j", t_44);
|
||||
frame.set("loop.index", t_41 + 1);
|
||||
frame.set("loop.index0", t_41);
|
||||
frame.set("loop.revindex", t_42 - t_41);
|
||||
frame.set("loop.revindex0", t_42 - t_41 - 1);
|
||||
frame.set("loop.first", t_41 === 0);
|
||||
frame.set("loop.last", t_41 === t_42 - 1);
|
||||
frame.set("loop.length", t_42);
|
||||
output += "\n <li id=\"certificate_";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_39),"sha256sum"), env.opts.autoescape);
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_44),"sha256sum"), env.opts.autoescape);
|
||||
output += "\">\n ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_39),"changed"), env.opts.autoescape);
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape);
|
||||
output += "\n ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_39),"serial_number"), env.opts.autoescape);
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape);
|
||||
output += " <span class=\"monospace\">";
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_39),"identity"), env.opts.autoescape);
|
||||
output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape);
|
||||
output += "</span>\n </li>\n ";
|
||||
;
|
||||
}
|
||||
}
|
||||
if (!t_37) {
|
||||
if (!t_42) {
|
||||
output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t ";
|
||||
}
|
||||
frame = frame.pop();
|
||||
output += "\n </ul>\n</section>\n\n<section id=\"config\">\n</section>\n";
|
||||
output += "\n </ul>\n</section>\n\n<section id=\"config\">\n</section>\n\n";
|
||||
;
|
||||
}
|
||||
output += "\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
cb(null, output);
|
||||
}
|
||||
})});
|
||||
;
|
||||
} catch (e) {
|
||||
cb(runtime.handleError(e, lineno, colno));
|
||||
}
|
||||
@ -894,8 +970,8 @@ var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<li id=\"request_";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
|
||||
output += "<li id=\"request-";
|
||||
output += runtime.suppressValue(env.getFilter("replace").call(context, env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"),"@","--"),".","-"), env.opts.autoescape);
|
||||
output += "\" class=\"filterable\">\n\n<a class=\"button icon download\" href=\"/api/request/";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
|
||||
output += "/\">Fetch</a>\n";
|
||||
@ -912,7 +988,7 @@ output += "\n<button title=\"Please use certidude command-line utility to sign u
|
||||
output += "\n<button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/request/";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"common_name"), env.opts.autoescape);
|
||||
output += "/',type:'delete'});\">Delete</button>\n\n\n<div class=\"monospace\">\n";
|
||||
env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/request.html", null, function(t_3,t_1) {
|
||||
env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/request.html", null, function(t_3,t_1) {
|
||||
if(t_3) { cb(t_3); return; }
|
||||
t_1.render(context.getVariables(), frame, function(t_4,t_2) {
|
||||
if(t_4) { cb(t_4); return; }
|
||||
@ -920,9 +996,9 @@ output += t_2
|
||||
output += "\n";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"identity"), env.opts.autoescape);
|
||||
output += "\n</div>\n\n";
|
||||
(function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) {
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address")) {
|
||||
output += "\n<div class=\"email\">";
|
||||
env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/request.html", null, function(t_7,t_5) {
|
||||
env.getTemplate("img/iconmonstr-email-2.svg", false, "views/request.html", null, function(t_7,t_5) {
|
||||
if(t_7) { cb(t_7); return; }
|
||||
t_5.render(context.getVariables(), frame, function(t_8,t_6) {
|
||||
if(t_8) { cb(t_8); return; }
|
||||
@ -930,12 +1006,10 @@ output += t_6
|
||||
output += " ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"email_address"), env.opts.autoescape);
|
||||
output += "</div>\n";
|
||||
cb()})});
|
||||
})});
|
||||
}
|
||||
else {
|
||||
cb()}
|
||||
})(function() {output += "\n\n<div class=\"monospace\">\n";
|
||||
env.getTemplate("img/iconmonstr-key-2-icon.svg", false, "views/request.html", null, function(t_11,t_9) {
|
||||
output += "\n\n<div class=\"monospace\">\n";
|
||||
env.getTemplate("img/iconmonstr-key-3.svg", false, "views/request.html", null, function(t_11,t_9) {
|
||||
if(t_11) { cb(t_11); return; }
|
||||
t_9.render(context.getVariables(), frame, function(t_12,t_10) {
|
||||
if(t_12) { cb(t_12); return; }
|
||||
@ -957,9 +1031,9 @@ if(frame.topLevel) {
|
||||
context.addExport("key_usage", t_13);
|
||||
}
|
||||
output += "\n";
|
||||
(function(cb) {if(runtime.contextOrFrameLookup(context, frame, "key_usage")) {
|
||||
if(runtime.contextOrFrameLookup(context, frame, "key_usage")) {
|
||||
output += "\n<div>\n";
|
||||
env.getTemplate("img/iconmonstr-flag-3-icon.svg", false, "views/request.html", null, function(t_16,t_14) {
|
||||
env.getTemplate("img/iconmonstr-flag-3.svg", false, "views/request.html", null, function(t_16,t_14) {
|
||||
if(t_16) { cb(t_16); return; }
|
||||
t_14.render(context.getVariables(), frame, function(t_17,t_15) {
|
||||
if(t_17) { cb(t_17); return; }
|
||||
@ -967,17 +1041,15 @@ output += t_15
|
||||
output += "\n";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"key_usage"), env.opts.autoescape);
|
||||
output += "\n</div>\n";
|
||||
cb()})});
|
||||
})});
|
||||
}
|
||||
else {
|
||||
cb()}
|
||||
})(function() {output += "\n\n</li>\n\n";
|
||||
output += "\n\n</li>\n\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
cb(null, output);
|
||||
}
|
||||
})})})})})});
|
||||
})})})});
|
||||
} catch (e) {
|
||||
cb(runtime.handleError(e, lineno, colno));
|
||||
}
|
||||
@ -995,8 +1067,8 @@ var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<li id=\"certificate_";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"sha256sum"), env.opts.autoescape);
|
||||
output += "<li id=\"certificate-";
|
||||
output += runtime.suppressValue(env.getFilter("replace").call(context, env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"),"@","--"),".","-"), env.opts.autoescape);
|
||||
output += "\" data-dn=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape);
|
||||
output += "\" data-cn=\"";
|
||||
@ -1006,17 +1078,17 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook
|
||||
output += "/\">Fetch</a>\n <button class=\"icon revoke\" onClick=\"javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
|
||||
output += "/',type:'delete'});\">Revoke</button>\n\n <div class=\"monospace\">\n ";
|
||||
env.getTemplate("img/iconmonstr-certificate-15-icon.svg", false, "views/signed.html", null, function(t_3,t_1) {
|
||||
env.getTemplate("img/iconmonstr-certificate-15.svg", false, "views/signed.html", null, function(t_3,t_1) {
|
||||
if(t_3) { cb(t_3); return; }
|
||||
t_1.render(context.getVariables(), frame, function(t_4,t_2) {
|
||||
if(t_4) { cb(t_4); return; }
|
||||
output += t_2
|
||||
output += "\n ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"identity"), env.opts.autoescape);
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
|
||||
output += "\n </div>\n\n ";
|
||||
(function(cb) {if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) {
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address")) {
|
||||
output += "\n <div class=\"email\">";
|
||||
env.getTemplate("img/iconmonstr-email-2-icon.svg", false, "views/signed.html", null, function(t_7,t_5) {
|
||||
env.getTemplate("img/iconmonstr-email-2.svg", false, "views/signed.html", null, function(t_7,t_5) {
|
||||
if(t_7) { cb(t_7); return; }
|
||||
t_5.render(context.getVariables(), frame, function(t_8,t_6) {
|
||||
if(t_8) { cb(t_8); return; }
|
||||
@ -1024,32 +1096,53 @@ output += t_6
|
||||
output += " ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"email_address"), env.opts.autoescape);
|
||||
output += "</div>\n ";
|
||||
cb()})});
|
||||
})});
|
||||
}
|
||||
else {
|
||||
cb()}
|
||||
})(function() {output += "\n\n ";
|
||||
output += "\n\n <div class=\"tags\">\n <select class=\"icon tag\" data-cn=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
|
||||
output += "\" onChange=\"onNewTagClicked();\">\n <option value=\"\">Add tag...</option>\n ";
|
||||
env.getTemplate("views/tagtypes.html", false, "views/signed.html", null, function(t_11,t_9) {
|
||||
output += "\n \n ";
|
||||
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) {
|
||||
output += "\n <div class=\"person\">";
|
||||
env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) {
|
||||
if(t_11) { cb(t_11); return; }
|
||||
t_9.render(context.getVariables(), frame, function(t_12,t_10) {
|
||||
if(t_12) { cb(t_12); return; }
|
||||
output += t_10
|
||||
output += "\n </select>\n </div>\n\n <div class=\"status\">\n ";
|
||||
env.getTemplate("views/status.html", false, "views/signed.html", null, function(t_15,t_13) {
|
||||
output += " ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name"), env.opts.autoescape);
|
||||
output += " ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname"), env.opts.autoescape);
|
||||
output += "</div>\n ";
|
||||
})});
|
||||
}
|
||||
output += "\n\n <div class=\"lifetime\" title=\"Valid from ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"signed"), env.opts.autoescape);
|
||||
output += " to ";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"expires"), env.opts.autoescape);
|
||||
output += "\">\n ";
|
||||
env.getTemplate("img/iconmonstr-calendar-6.svg", false, "views/signed.html", null, function(t_15,t_13) {
|
||||
if(t_15) { cb(t_15); return; }
|
||||
t_13.render(context.getVariables(), frame, function(t_16,t_14) {
|
||||
if(t_16) { cb(t_16); return; }
|
||||
output += t_14
|
||||
output += "\n </div>\n</li>\n";
|
||||
output += "\n <time>";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"signed"), env.opts.autoescape);
|
||||
output += "</time> -\n <time>";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"expires"), env.opts.autoescape);
|
||||
output += "</time>\n </div>\n\n ";
|
||||
output += "\n\n <div class=\"tags\">\n <select class=\"icon tag\" data-cn=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"common_name"), env.opts.autoescape);
|
||||
output += "\" onChange=\"onNewTagClicked();\">\n <option value=\"\">Add tag...</option>\n ";
|
||||
env.getTemplate("views/tagtypes.html", false, "views/signed.html", null, function(t_19,t_17) {
|
||||
if(t_19) { cb(t_19); return; }
|
||||
t_17.render(context.getVariables(), frame, function(t_20,t_18) {
|
||||
if(t_20) { cb(t_20); return; }
|
||||
output += t_18
|
||||
output += "\n </select>\n </div>\n <div class=\"status\"></div>\n</li>\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
cb(null, output);
|
||||
}
|
||||
})})})})})})});
|
||||
})})})})})});
|
||||
} catch (e) {
|
||||
cb(runtime.handleError(e, lineno, colno));
|
||||
}
|
||||
@ -1135,6 +1228,44 @@ return {
|
||||
root: root
|
||||
};
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tags.html"] = (function() {
|
||||
function root(env, context, frame, runtime, cb) {
|
||||
var lineno = null;
|
||||
var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<span id=\"tag_";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"id"), env.opts.autoescape);
|
||||
output += "\" onclick=\"onTagClicked()\"\ntitle=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"), env.opts.autoescape);
|
||||
output += "=";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape);
|
||||
output += "\" class=\"";
|
||||
output += runtime.suppressValue(env.getFilter("replace").call(context, runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"),"."," "), env.opts.autoescape);
|
||||
output += "\"\ndata-id=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"id"), env.opts.autoescape);
|
||||
output += "\" data-key=\"";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"key"), env.opts.autoescape);
|
||||
output += "\">";
|
||||
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "tag")),"value"), env.opts.autoescape);
|
||||
output += "</span>\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
cb(null, output);
|
||||
}
|
||||
;
|
||||
} catch (e) {
|
||||
cb(runtime.handleError(e, lineno, colno));
|
||||
}
|
||||
}
|
||||
return {
|
||||
root: root
|
||||
};
|
||||
|
||||
})();
|
||||
})();
|
||||
(function() {(window.nunjucksPrecompiled = window.nunjucksPrecompiled || {})["views/tagtypes.html"] = (function() {
|
||||
@ -1144,7 +1275,7 @@ var colno = null;
|
||||
var output = "";
|
||||
try {
|
||||
var parentTemplate = null;
|
||||
output += "<option value=\"location\">Location</option>\n<option value=\"phone\">Phone</option>\n<option value=\"room\">Room</option>\n<option value=\"serial\">Product serial</option>\n\n<option value=\"wireless.protected.password\">Protected wireless network password</option>\n<option value=\"wireless.protected.name\">Protected wireless network name</option>\n<option value=\"wireless.public.name\">Public wireless network name</option>\n<option value=\"wireless.channela\">5GHz channel number</option>\n<option value=\"wireless.channelb\">2.4GHz channel number</option>\n<option value=\"usb.approved\">Approved USB device</option>\n";
|
||||
output += "<option value=\"location\">Location</option>\n<option value=\"phone\">Phone</option>\n<option value=\"room\">Room</option>\n<option value=\"serial\">Product serial</option>\n\n<option value=\"wireless.protected.password\">Protected wireless network password</option>\n<option value=\"wireless.protected.name\">Protected wireless network name</option>\n<option value=\"wireless.public.name\">Public wireless network name</option>\n<option value=\"wireless.channel\">Channel number</option>\n<option value=\"usb.approved\">Approved USB device</option>\n";
|
||||
if(parentTemplate) {
|
||||
parentTemplate.rootRenderFunc(env, context, frame, runtime, cb);
|
||||
} else {
|
||||
|
2
certidude/static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
@ -1,36 +1,127 @@
|
||||
|
||||
<section id="about">
|
||||
<p>Hi {{session.username}},</p>
|
||||
<h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2>
|
||||
|
||||
<p>Request submission is allowed from: {% if session.request_subnets %}{% for i in session.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p>
|
||||
<p>Autosign is allowed from: {% if session.autosign_subnets %}{% for i in session.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p>
|
||||
<p>Authority administration is allowed from: {% if session.admin_subnets %}{% for i in session.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}
|
||||
<p>Authority administration allowed for: {% for i in session.admin_users %}{{ i }} {% endfor %}</p>
|
||||
<p>Mails will be sent to: {{ session.user.mail }}</p>
|
||||
|
||||
<p>You can click <a href="/api/bundle/">here</a> to generate bundle
|
||||
for current user account.</p>
|
||||
|
||||
{% if session.authority %}
|
||||
|
||||
<h2>Authority certificate</h2>
|
||||
|
||||
<p>Several things such as CRL location and e-mails are hardcoded into
|
||||
the <a href="/api/certificate">certificate</a> and
|
||||
as such require complete reset of X509 infrastructure if some of them needs to be changed:</p>
|
||||
|
||||
<p>Mails will appear from: {{ session.authority.certificate.email_address }}</p>
|
||||
|
||||
|
||||
<h2>Authority settings</h2>
|
||||
|
||||
<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>
|
||||
|
||||
<p>Outgoing mail server:
|
||||
{% if session.authority.outbox %}
|
||||
{{ session.authority.outbox }}
|
||||
{% else %}
|
||||
E-mail disabled
|
||||
{% endif %}</p>
|
||||
|
||||
<p>Authenticated users allowed from:
|
||||
|
||||
{% if "0.0.0.0/0" in session.user_subnets %}
|
||||
anywhere
|
||||
</p>
|
||||
{% else %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for i in session.user_subnets %}
|
||||
<li>{{ i }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<p>Request submission is allowed from:
|
||||
|
||||
{% if "0.0.0.0/0" in session.request_subnets %}
|
||||
anywhere
|
||||
</p>
|
||||
{% else %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for subnet in session.request_subnets %}
|
||||
<li>{{ subnet }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p>Autosign is allowed from:
|
||||
{% if "0.0.0.0/0" in session.autosign_subnets %}
|
||||
anywhere
|
||||
</p>
|
||||
{% else %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for subnet in session.autosign_subnets %}
|
||||
<li>{{ subnet }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p>Authority administration is allowed from:
|
||||
{% if "0.0.0.0/0" in session.admin_subnets %}
|
||||
anywhere
|
||||
</p>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for subnet in session.admin_subnets %}
|
||||
<li>{{ subnet }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p>Authority administration allowed for:</p>
|
||||
|
||||
<ul>
|
||||
{% for handle, full_name in session.admin_users %}
|
||||
<li>{{ full_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
<p>Here you can renew your certificates</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% set s = session.certificate.identity %}
|
||||
|
||||
|
||||
{% if session.authority %}
|
||||
<section id="requests">
|
||||
<h1>Pending requests</h1>
|
||||
|
||||
<p>Submit a certificate signing request with Certidude:</p>
|
||||
<pre>certidude setup client {{session.common_name}}</pre>
|
||||
|
||||
<ul id="pending_requests">
|
||||
{% for request in session.requests %}
|
||||
{% for request in session.authority.requests %}
|
||||
{% include "views/request.html" %}
|
||||
{% endfor %}
|
||||
<li class="notify">
|
||||
<p>No certificate signing requests to sign! You can submit a certificate signing request by:</p>
|
||||
<pre>certidude setup client {{session.common_name}}</pre>
|
||||
<p>No certificate signing requests to sign!</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="signed">
|
||||
<h1>Signed certificates</h1>
|
||||
<input id="search" type="search" class="icon search">
|
||||
<ul id="signed_certificates">
|
||||
{% for certificate in session.signed | sort | reverse %}
|
||||
{% for certificate in session.authority.signed | sort | reverse %}
|
||||
{% include "views/signed.html" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -62,7 +153,7 @@
|
||||
</pre>
|
||||
-->
|
||||
<ul>
|
||||
{% for j in session.revoked %}
|
||||
{% for j in session.authority.revoked %}
|
||||
<li id="certificate_{{ j.sha256sum }}">
|
||||
{{j.changed}}
|
||||
{{j.serial_number}} <span class="monospace">{{j.identity}}</span>
|
||||
@ -75,3 +166,5 @@
|
||||
|
||||
<section id="config">
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<li id="request_{{ request.common_name }}" class="filterable">
|
||||
<li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
|
||||
|
||||
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
|
||||
{% if request.signable %}
|
||||
@ -10,16 +10,16 @@
|
||||
|
||||
|
||||
<div class="monospace">
|
||||
{% include 'img/iconmonstr-certificate-15-icon.svg' %}
|
||||
{% include 'img/iconmonstr-certificate-15.svg' %}
|
||||
{{request.identity}}
|
||||
</div>
|
||||
|
||||
{% if request.email_address %}
|
||||
<div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ request.email_address }}</div>
|
||||
<div class="email">{% include 'img/iconmonstr-email-2.svg' %} {{ request.email_address }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="monospace">
|
||||
{% include 'img/iconmonstr-key-2-icon.svg' %}
|
||||
{% include 'img/iconmonstr-key-3.svg' %}
|
||||
<span title="SHA-1 of public key">
|
||||
{{ request.sha256sum }}
|
||||
</span>
|
||||
@ -30,7 +30,7 @@
|
||||
{% set key_usage = request.key_usage %}
|
||||
{% if key_usage %}
|
||||
<div>
|
||||
{% include 'img/iconmonstr-flag-3-icon.svg' %}
|
||||
{% include 'img/iconmonstr-flag-3.svg' %}
|
||||
{{request.key_usage}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -1,20 +1,30 @@
|
||||
<li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable">
|
||||
<li id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable">
|
||||
<a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a>
|
||||
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button>
|
||||
|
||||
<div class="monospace">
|
||||
{% include 'img/iconmonstr-certificate-15-icon.svg' %}
|
||||
{{certificate.identity}}
|
||||
{% include 'img/iconmonstr-certificate-15.svg' %}
|
||||
{{certificate.common_name}}
|
||||
</div>
|
||||
|
||||
{% if certificate.email_address %}
|
||||
<div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ certificate.email_address }}</div>
|
||||
<div class="email">{% include 'img/iconmonstr-email-2.svg' %} {{ certificate.email_address }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if certificate.given_name or certificate.surname %}
|
||||
<div class="person">{% include 'img/iconmonstr-user-5.svg' %} {{ certificate.given_name }} {{ certificate.surname }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="lifetime" title="Valid from {{ certificate.signed }} to {{ certificate.expires }}">
|
||||
{% include 'img/iconmonstr-calendar-6.svg' %}
|
||||
<time>{{ certificate.signed }}</time> -
|
||||
<time>{{ certificate.expires }}</time>
|
||||
</div>
|
||||
|
||||
{#
|
||||
|
||||
<div class="monospace">
|
||||
{% include 'img/iconmonstr-key-2-icon.svg' %}
|
||||
{% include 'img/iconmonstr-key-3.svg' %}
|
||||
<span title="SHA-256 of public key">
|
||||
{{ certificate.sha256sum }}
|
||||
</span>
|
||||
@ -23,20 +33,17 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% include 'img/iconmonstr-flag-3-icon.svg' %}
|
||||
{% include 'img/iconmonstr-flag-3.svg' %}
|
||||
{{certificate.key_usage}}
|
||||
</div>
|
||||
|
||||
#}
|
||||
|
||||
<div class="tags">
|
||||
<select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();">
|
||||
<select class="icon tag" data-cn="{{ certificate.common_name }}" onChange="onNewTagClicked();">
|
||||
<option value="">Add tag...</option>
|
||||
{% include 'views/tagtypes.html' %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
{% include 'views/status.html' %}
|
||||
{% include 'views/tagtypes.html' %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="status"></div>
|
||||
</li>
|
||||
|
3
certidude/static/views/tags.html
Normal file
@ -0,0 +1,3 @@
|
||||
<span id="tag_{{ tag.id }}" onclick="onTagClicked()"
|
||||
title="{{ tag.key }}={{ tag.value }}" class="{{ tag.key | replace('.', ' ') }}"
|
||||
data-id="{{ tag.id }}" data-key="{{ tag.key }}">{{ tag.value }}</span>
|
@ -6,6 +6,5 @@
|
||||
<option value="wireless.protected.password">Protected wireless network password</option>
|
||||
<option value="wireless.protected.name">Protected wireless network name</option>
|
||||
<option value="wireless.public.name">Public wireless network name</option>
|
||||
<option value="wireless.channela">5GHz channel number</option>
|
||||
<option value="wireless.channelb">2.4GHz channel number</option>
|
||||
<option value="wireless.channel">Channel number</option>
|
||||
<option value="usb.approved">Approved USB device</option>
|
||||
|
@ -1,20 +1,60 @@
|
||||
[authentication]
|
||||
backends = pam
|
||||
#backends = kerberos
|
||||
#backends = ldap
|
||||
#backends = kerberos ldap
|
||||
#backends = kerberos pam
|
||||
|
||||
[accounts]
|
||||
backend = posix
|
||||
#backend = ldap
|
||||
|
||||
[authorization]
|
||||
admin_users = administrator
|
||||
admin_subnets = 0.0.0.0/0
|
||||
request_subnets = 0.0.0.0/0
|
||||
autosign_subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||
backend = posix
|
||||
#backend = ldap
|
||||
whitelist admin users = root administrator
|
||||
ldap gssapi credential cache = /run/certidude/krb5cc
|
||||
|
||||
ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s))
|
||||
ldap user filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s))
|
||||
ldap admins filter = (&(objectclass=user)(objectclass=person)(memberOf=cn=Domain Admins,cn=Users,dc=koodur,dc=com)(samaccountname=%s))
|
||||
ldap member of filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s)(memberOf=%s))
|
||||
ldap members filter = (&(objectclass=group)(cn=%s)(member=%s))
|
||||
|
||||
ldap group filter = (&(objectClass=group)(cn=%s)(member=%s))
|
||||
ldap user group =
|
||||
ldap admin group = domain admins
|
||||
posix user group =
|
||||
posix admin group = certidude
|
||||
user subnets = 0.0.0.0/0
|
||||
admin subnets = 0.0.0.0/0
|
||||
request subnets = 0.0.0.0/0
|
||||
autosign subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||
|
||||
[logging]
|
||||
backend = sql
|
||||
database = sqlite://{{ directory }}/db.sqlite
|
||||
|
||||
[tagging]
|
||||
backend = sql
|
||||
database = sqlite://{{ directory }}/db.sqlite
|
||||
|
||||
[leases]
|
||||
backend = sql
|
||||
schema = strongswan
|
||||
database = sqlite://{{ directory }}/db.sqlite
|
||||
|
||||
[signature]
|
||||
certificate_lifetime = 1825
|
||||
revocation_list_lifetime = 1
|
||||
certificate lifetime = 1825
|
||||
revocation list lifetime = 1
|
||||
|
||||
[push]
|
||||
server =
|
||||
|
||||
[authority]
|
||||
private_key_path = {{ ca_key }}
|
||||
certificate_path = {{ ca_crt }}
|
||||
requests_dir = {{ directory }}/requests/
|
||||
signed_dir = {{ directory }}/signed/
|
||||
revoked_dir = {{ directory }}/revoked/
|
||||
|
||||
private key path = {{ ca_key }}
|
||||
certificate path = {{ ca_crt }}
|
||||
requests dir = {{ directory }}/requests/
|
||||
signed dir = {{ directory }}/signed/
|
||||
revoked dir = {{ directory }}/revoked/
|
||||
outbox = smtp://localhost
|
||||
|
7
certidude/templates/mail/certificate-signed.md
Normal file
@ -0,0 +1,7 @@
|
||||
Certificate {{certificate.common_name}} ({{certificate.serial_number}}) signed
|
||||
|
||||
This is simply to notify that certificate {{ certificate.common_name }}
|
||||
was signed{% if signer %} by {{ signer }}{% endif %}.
|
||||
|
||||
Any existing certificates with the same common name were rejected by doing so
|
||||
and services making use of those certificates might become unavailable.
|
20
certidude/templates/nginx-https-site.conf
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{constants.FQDN}};
|
||||
rewrite ^ https://{{constants.FQDN}}$request_uri?;
|
||||
}
|
||||
|
||||
server {
|
||||
root /var/www/html;
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
|
||||
listen 443 ssl;
|
||||
server_name {{constants.FQDN}};
|
||||
client_max_body_size 10G;
|
||||
ssl_certificate {{certificate_path}};
|
||||
ssl_certificate_key {{key_path}};
|
||||
ssl_client_certificate {{authority_path}};
|
||||
ssl_verify_client {{verify_client}};
|
||||
}
|
||||
|
6
certidude/templates/nginx-tls.conf
Normal file
@ -0,0 +1,6 @@
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
||||
ssl_dhparam {{dhparam_path}};
|
||||
|
@ -34,15 +34,21 @@ http {
|
||||
}
|
||||
|
||||
{% if not push_server %}
|
||||
location ~ /publish/(.*) {
|
||||
location /pub {
|
||||
allow 127.0.0.1;
|
||||
push_stream_publisher admin;
|
||||
push_stream_channels_path $1;
|
||||
nchan_publisher http;
|
||||
nchan_store_messages off;
|
||||
nchan_channel_id $arg_id;
|
||||
}
|
||||
|
||||
location ~ /subscribe/(.*) {
|
||||
push_stream_channels_path $1;
|
||||
push_stream_subscriber long-polling;
|
||||
location ~ "^/lp/(.*)" {
|
||||
nchan_subscriber longpoll;
|
||||
nchan_channel_id $1;
|
||||
}
|
||||
|
||||
location ~ "^/ev/(.*)" {
|
||||
nchan_subscriber eventsource;
|
||||
nchan_channel_id $1;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
[uwsgi]
|
||||
exec-as-root = /usr/local/bin/certidude spawn
|
||||
exec-as-root = /usr/local/bin/certidude signer spawn -k
|
||||
master = true
|
||||
processes = 1
|
||||
vacuum = true
|
||||
@ -15,3 +15,5 @@ buffer-size = 32768
|
||||
env = LANG=C.UTF-8
|
||||
env = LC_ALL=C.UTF-8
|
||||
env = KRB5_KTNAME={{kerberos_keytab}}
|
||||
env = KRB5CCNAME=/run/certidude/krb5cc
|
||||
|
||||
|
@ -3,10 +3,9 @@ import hashlib
|
||||
import re
|
||||
import click
|
||||
import io
|
||||
from Crypto.Util import asn1
|
||||
from certidude import constants
|
||||
from OpenSSL import crypto
|
||||
from datetime import datetime
|
||||
from certidude.signer import raw_sign, EXTENSION_WHITELIST
|
||||
|
||||
def subject2dn(subject):
|
||||
bits = []
|
||||
@ -16,6 +15,10 @@ def subject2dn(subject):
|
||||
return ", ".join(bits)
|
||||
|
||||
class CertificateBase:
|
||||
# Others will cause browsers to import the cert instead of offering to
|
||||
# download it
|
||||
content_type = "application/x-pem-file"
|
||||
|
||||
def __repr__(self):
|
||||
return self.buf
|
||||
|
||||
@ -41,7 +44,7 @@ class CertificateBase:
|
||||
|
||||
@common_name.setter
|
||||
def common_name(self, value):
|
||||
return setattr(self._obj.get_subject(), "CN", value)
|
||||
self.subject.CN = value
|
||||
|
||||
@property
|
||||
def country_code(self):
|
||||
@ -130,7 +133,6 @@ class CertificateBase:
|
||||
def set_extensions(self, extensions):
|
||||
# X509Req().add_extensions() first invocation takes only effect?!
|
||||
assert self._obj.get_extensions() == [], "Extensions already set!"
|
||||
|
||||
self._obj.add_extensions([
|
||||
crypto.X509Extension(
|
||||
key.encode("ascii"),
|
||||
@ -164,6 +166,7 @@ class CertificateBase:
|
||||
|
||||
@property
|
||||
def pubkey(self):
|
||||
from Crypto.Util import asn1
|
||||
pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey())
|
||||
pubkey_der=asn1.DerSequence()
|
||||
pubkey_der.decode(pubkey_asn1)
|
||||
@ -194,6 +197,11 @@ class CertificateBase:
|
||||
|
||||
|
||||
class Request(CertificateBase):
|
||||
|
||||
@property
|
||||
def suggested_filename(self):
|
||||
return self.common_name + ".csr"
|
||||
|
||||
def __init__(self, mixed=None):
|
||||
self.buf = None
|
||||
self.path = NotImplemented
|
||||
@ -204,27 +212,23 @@ class Request(CertificateBase):
|
||||
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
|
||||
self.created = datetime.fromtimestamp(mtime)
|
||||
mixed = mixed.read()
|
||||
if isinstance(mixed, bytes):
|
||||
mixed = mixed.decode("ascii")
|
||||
if isinstance(mixed, str):
|
||||
try:
|
||||
self.buf = mixed
|
||||
mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed)
|
||||
except crypto.Error:
|
||||
print("Failed to parse:", mixed)
|
||||
raise
|
||||
|
||||
raise ValueError("Failed to parse: %s" % mixed)
|
||||
if isinstance(mixed, crypto.X509Req):
|
||||
self._obj = mixed
|
||||
else:
|
||||
raise ValueError("Can't parse %s as X.509 certificate signing request!" % mixed)
|
||||
raise ValueError("Can't parse %s (%s) as X.509 certificate signing request!" % (mixed, type(mixed)))
|
||||
|
||||
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump()))
|
||||
|
||||
@property
|
||||
def signable(self):
|
||||
for key, value, data in self.extensions:
|
||||
if key not in EXTENSION_WHITELIST:
|
||||
if key not in constants.EXTENSION_WHITELIST:
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -243,6 +247,11 @@ class Request(CertificateBase):
|
||||
|
||||
|
||||
class Certificate(CertificateBase):
|
||||
|
||||
@property
|
||||
def suggested_filename(self):
|
||||
return self.common_name + ".crt"
|
||||
|
||||
def __init__(self, mixed):
|
||||
self.buf = NotImplemented
|
||||
self.path = NotImplemented
|
||||
@ -253,15 +262,12 @@ class Certificate(CertificateBase):
|
||||
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
|
||||
self.changed = datetime.fromtimestamp(mtime)
|
||||
mixed = mixed.read()
|
||||
|
||||
if isinstance(mixed, str):
|
||||
try:
|
||||
self.buf = mixed
|
||||
mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed)
|
||||
except crypto.Error:
|
||||
print("Failed to parse:", mixed)
|
||||
raise
|
||||
|
||||
raise ValueError("Failed to parse: %s" % mixed)
|
||||
if isinstance(mixed, crypto.X509):
|
||||
self._obj = mixed
|
||||
else:
|
||||
|
@ -1,20 +1,20 @@
|
||||
cffi==1.2.1
|
||||
click==5.1
|
||||
configparser==3.3.0r2
|
||||
cryptography==1.0
|
||||
falcon==0.3.0
|
||||
future=0.15.2
|
||||
humanize==0.5.1
|
||||
idna==2.0
|
||||
ipaddress==1.0.16
|
||||
ipsecparse==0.1.0
|
||||
Jinja2==2.8
|
||||
ldap3==0.9.8.8
|
||||
Markdown==2.6.5
|
||||
MarkupSafe==0.23
|
||||
pyasn1==0.1.8
|
||||
pycountry==1.14
|
||||
pycparser==2.14
|
||||
pycrypto==2.6.1
|
||||
pykerberos==1.1.8
|
||||
pyOpenSSL==0.15.1
|
||||
python-ldap==2.4.10
|
||||
python-mimeparse==0.1.4
|
||||
requests==2.2.1
|
||||
setproctitle==1.1.9
|
||||
|