Commit fee3b4ea authored by Aleš Mrázek's avatar Aleš Mrázek
Browse files

Merge branch 'ssl' into 'master'

DISABLE_SSL and CLIENT_CN options

See merge request !1
parents 5534fc65 da6e0282
.. |date| date::
*******
JetConf
Jetconf
*******
:Author: Pavel Špírek <pavel.spirek@nic.cz>
:Date: |date|
Jetconf is an implementation of the RESTCONF_ protocol written in
Python 3.
*JetConf* is an implementation of the RESTCONF_ protocol written in
Python 3. Main features:
Main features:
* HTTP/2 over TLS, certificate-based authentication of clients
......@@ -21,10 +18,10 @@ Python 3. Main features:
Requirements
=============
*JetConf* requires Python 3.6 or newer::
Jetconf requires **Python 3.6 or newer**::
$ sudo apt-get install python3
$ sudo apt-get install python3-pip
~$ apt-get install python3
~$ apt-get install python3-pip
These requirements should be installed by running *Instalation*
......@@ -36,47 +33,35 @@ These requirements should be installed by running *Instalation*
pytz
PyYAML
yangson
Installation
============
*JetConf* can be installed by PyPI:
::
Jetconf can be installed by PyPI::
$ python3 -m pip install jetconf
~$ python3 -m pip install jetconf
Running
=======
Running *JetConf*
Running Jetconf::
::
~$ jetconf -c <path_to_config_file.yaml>
$ jetconf -c <path_to_config_file.yaml>
For development purposes, Jetconf can also be started directly
from git repository with ``run.py`` script.::
For development purposes, *JetConf* can also be started directly
from Git repository with run.py script:
~$ ./run.py -c <path_to_config_file.yaml>
::
$ ./run.py -c <path_to_config_file.yaml>
Example configuration (template)
================================
In the 'data' folder, there is an example template for
In the ``data`` folder, there is an example template ``example-config.yaml`` for
configuring paths, certificates etc.
::
example-config.yaml
In this configuration file, you have to modify all paths to match
your actual file locations.
......@@ -84,10 +69,10 @@ your actual file locations.
Links
=====
* `Git repository`_
* `GitHub repository`_
* `Documentation`_
.. _RESTCONF: https://tools.ietf.org/html/draft-ietf-netconf-restconf-18
.. _NACM: https://datatracker.ietf.org/doc/rfc6536/
.. _Git repository: https://github.com/CZ-NIC/jetconf
.. _GitHub repository: https://github.com/CZ-NIC/jetconf
.. _Documentation: https://gitlab.labs.nic.cz/labs/jetconf/wikis/home
......@@ -4,24 +4,20 @@ GLOBAL:
PIDFILE: "/tmp/jetconf.pid"
PERSISTENT_CHANGES: false
LOG_LEVEL: "debug"
LOG_DBG_MODULES: ["usr_conf_data_handlers", "knot_api", "nacm", "data"]
YANG_LIB_DIR: "/home/user/jetconf-conf/yang-data-jukebox/"
DATA_JSON_FILE: "/home/user/jetconf-conf/data.json.jb"
LOG_DBG_MODULES: ["usr_conf_data_handlers", "nacm", "data"]
YANG_LIB_DIR: "yang-modules"
DATA_JSON_FILE: "data.json"
BACKEND_PACKAGE: "jetconf_jukebox"
HTTP_SERVER:
DOC_ROOT: "/home/user/jetconf-conf/doc-root"
DOC_ROOT: "doc-root"
DOC_DEFAULT_NAME: "index.html"
API_ROOT: "/restconf"
SERVER_NAME: "jetconf-h2"
SERVER_SSL_CERT: "/home/user/jetconf-conf/server_localhost.crt"
SERVER_SSL_PRIVKEY: "/home/user/jetconf-conf/server_localhost.key"
CA_CERT: "/home/user/jetconf-conf/ca.pem"
DBG_DISABLE_CERTS: false
SERVER_SSL_CERT: "server_localhost.crt"
SERVER_SSL_PRIVKEY: "server_localhost.key"
CA_CERT: "ca.pem"
NACM:
ENABLED: true
ALLOWED_USERS: ["example@mail.cz"]
KNOT:
SOCKET: "/home/user/knot-conf/knot.sock"
......@@ -3,6 +3,7 @@ import yaml
from colorlog import info
from yaml.parser import ParserError
from yaml.loader import SafeLoader
CFG = None # type: JcConfig
......@@ -21,6 +22,7 @@ class JcConfig:
"YANG_LIB_DIR": yang_mod_dir_env,
"DATA_JSON_FILE": "data.json",
"VALIDATE_TRANSACTIONS": True,
"CLIENT_CN": False,
"BACKEND_PACKAGE": "jetconf_jukebox"
}
......@@ -33,11 +35,11 @@ class JcConfig:
"UPLOAD_SIZE_LIMIT": 1,
"LISTEN_LOCALHOST_ONLY": False,
"PORT": 8443,
"DISABLE_SSL": False,
"DBG_DISABLE_CERT": False,
"SERVER_SSL_CERT": "server.crt",
"SERVER_SSL_PRIVKEY": "server.key",
"CA_CERT": "ca.pem",
"DBG_DISABLE_CERTS": False
}
nacm_def = {
......@@ -75,7 +77,7 @@ class JcConfig:
def load_file(self, file_path: str) -> bool:
with open(file_path) as conf_fd:
try:
conf_yaml = yaml.load(conf_fd)
conf_yaml = yaml.load(conf_fd, Loader=SafeLoader)
except ParserError as e:
raise ValueError(str(e))
......
import re
import sys
import logging
from collections import OrderedDict
from colorlog import debug, getLogger
from enum import Enum
......@@ -21,12 +23,36 @@ class PathFormat(Enum):
XPATH = 1
class CertHelpers:
class ClientHelpers:
@staticmethod
def get_field(cert: SSLCertT, key: str) -> str:
if config.CFG.http["DBG_DISABLE_CERTS"] and (key == "emailAddress"):
def get_username(client_cert: SSLCertT, headers: OrderedDict):
if config.CFG.http["DBG_DISABLE_CERT"]:
return "test-user"
if config.CFG.glob["CLIENT_CN"]:
return ClientHelpers.get_common_name(client_cert, headers)
else:
return ClientHelpers.get_email_address(client_cert, headers)
@staticmethod
def get_email_address(client_cert: SSLCertT, headers: OrderedDict):
if config.CFG.http["DISABLE_SSL"]:
h_dn = HeadersHelper.get_header(headers, "x-ssl-client-dn")
return re.search(r'emailAddress=(.*?)(\/|$)', h_dn).group(1)
else:
return CertHelpers.get_field(client_cert, "emailAddress")
@staticmethod
def get_common_name(client_cert: SSLCertT, headers: OrderedDict):
if config.CFG.http["DISABLE_SSL"]:
return HeadersHelper.get_header(headers, "x-ssl-client-cn")
else:
return CertHelpers.get_field(client_cert, "commonName")
class CertHelpers:
@staticmethod
def get_field(cert: SSLCertT, key: str) -> str:
try:
retval = ([x[0][1] for x in cert["subject"] if x[0][0] == key] or [None])[0]
except (IndexError, KeyError, TypeError):
......@@ -34,6 +60,17 @@ class CertHelpers:
return retval
class HeadersHelper:
@staticmethod
def get_header(headers: OrderedDict, key: str) -> str:
try:
retval = headers.get(key)
except (IndexError, KeyError, TypeError):
retval = None
return retval
class DataHelpers:
# Get the namespace of the first segment in path
# Raises ValueError if the first segment is not in fully-qualified format
......
......@@ -14,7 +14,7 @@ from yangson.instance import NonexistentInstance, InstanceValueError, RootNode,
from yangson.instvalue import ArrayValue
from . import config
from .helpers import CertHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
from .helpers import ClientHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
from .journal import RpcInfo
from .data import BaseDatastore, ChangeType
from .errors import (
......@@ -351,7 +351,7 @@ class HttpHandlersImpl:
return http_resp
def get_api_running(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_get_running: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_running_data):]
......@@ -359,7 +359,7 @@ class HttpHandlersImpl:
return http_resp
def get_api_staging(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_get_staging: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -367,7 +367,7 @@ class HttpHandlersImpl:
return http_resp
def get_api_op(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] get_op: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_ops):].rstrip("/")
......@@ -416,7 +416,7 @@ class HttpHandlersImpl:
def get_file(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
# Ordinary file on filesystem
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
url_path = headers[":path"].split("?")[0]
url_path_safe = "".join(filter(lambda c: c.isalpha() or c in "/-_.", url_path)).replace("..", "").strip("/")
file_path = os.path.join(config.CFG.http["DOC_ROOT"], url_path_safe)
......@@ -596,7 +596,7 @@ class HttpHandlersImpl:
return http_resp
def post_api(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_post: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -677,7 +677,7 @@ class HttpHandlersImpl:
return http_resp
def put_api(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_put: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -745,7 +745,7 @@ class HttpHandlersImpl:
return http_resp
def delete_api(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] api_delete: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_data):]
......@@ -753,7 +753,7 @@ class HttpHandlersImpl:
return http_resp
def post_api_op_call(self, headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
username = CertHelpers.get_field(client_cert, "emailAddress")
username = ClientHelpers.get_username(client_cert, headers)
info("[{}] invoke_op: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(config.CFG.api_root_ops):]
......
......@@ -10,7 +10,8 @@ from h2.config import H2Configuration
from h2.connection import H2Connection
from h2.errors import ErrorCodes as H2ErrorCodes
from h2.exceptions import ProtocolError
from h2.events import DataReceived, RequestReceived, RemoteSettingsChanged, StreamEnded, WindowUpdated, ConnectionTerminated
from h2.events import DataReceived, RequestReceived, RemoteSettingsChanged, \
StreamEnded, WindowUpdated, ConnectionTerminated
from . import config
from .helpers import SSLCertT, LogHelpers
......@@ -57,17 +58,20 @@ class H2Protocol(asyncio.Protocol):
self.transport = transport
self.client_cert = transport.get_extra_info("peercert")
ssl_context = transport.get_extra_info("ssl_object")
if ssl.HAS_ALPN:
agreed_protocol = ssl_context.selected_alpn_protocol()
else:
agreed_protocol = ssl_context.selected_npn_protocol()
if agreed_protocol is None:
error("Connection error, client does not support HTTP/2")
self.transport.close()
else:
if config.CFG.http["DISABLE_SSL"]:
self.conn.initiate_connection()
else:
ssl_context = transport.get_extra_info("ssl_object")
if ssl.HAS_ALPN:
agreed_protocol = ssl_context.selected_alpn_protocol()
else:
agreed_protocol = ssl_context.selected_npn_protocol()
if agreed_protocol is None:
error("Connection error, client does not support HTTP/2")
self.transport.close()
else:
self.conn.initiate_connection()
def data_received(self, data: bytes):
events = self.conn.receive_data(data)
......@@ -272,21 +276,25 @@ class H2Protocol(asyncio.Protocol):
class RestServer:
def __init__(self):
# HTTP server init
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.options |= (ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION)
ssl_context.load_cert_chain(certfile=config.CFG.http["SERVER_SSL_CERT"], keyfile=config.CFG.http["SERVER_SSL_PRIVKEY"])
if ssl.HAS_ALPN:
ssl_context.set_alpn_protocols(["h2"])
# HTTP server init
if config.CFG.http["DISABLE_SSL"]:
ssl_context = None
else:
info("Python not compiled with ALPN support, using NPN instead.")
ssl_context.set_npn_protocols(["h2"])
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.options |= (ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_COMPRESSION)
ssl_context.load_cert_chain(certfile=config.CFG.http["SERVER_SSL_CERT"],
keyfile=config.CFG.http["SERVER_SSL_PRIVKEY"])
if ssl.HAS_ALPN:
ssl_context.set_alpn_protocols(["h2"])
else:
info("Python not compiled with ALPN support, using NPN instead.")
ssl_context.set_npn_protocols(["h2"])
if not config.CFG.http["DBG_DISABLE_CERTS"]:
ssl_context.verify_mode = ssl.CERT_REQUIRED
if not config.CFG.http["DBG_DISABLE_CERT"]:
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.load_verify_locations(cafile=config.CFG.http["CA_CERT"])
ssl_context.load_verify_locations(cafile=config.CFG.http["CA_CERT"])
self.loop = asyncio.get_event_loop()
......
......@@ -7,24 +7,24 @@ with codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8') as readme:
long_description = readme.read()
setup(
name = "jetconf",
packages = find_packages(),
use_scm_version = True,
name="jetconf",
packages=find_packages(),
use_scm_version=True,
setup_requires=["setuptools_scm"],
description = "Pure Python implementation of RESTCONF server",
long_description = long_description,
url = "https://gitlab.labs.nic.cz/labs/jetconf",
author = "Pavel Spirek",
author_email = "pavel.spirek@nic.cz",
entry_points = {
description="Pure Python implementation of RESTCONF server",
long_description=long_description,
url="https://gitlab.labs.nic.cz/labs/jetconf",
author="Ales Mrazek",
author_email="ales.mrazek@nic.cz",
entry_points={
"console_scripts": ["jetconf=jetconf.__main__:main"]
},
install_requires = ["yangson", "h2", "colorlog", "pyaml", "pytz"],
tests_require = ["pytest"],
keywords = ["RESTCONF", "yang", "data model", "configuration", "json"],
classifiers = [
install_requires=["yangson", "h2", "colorlog", "pyaml", "pytz"],
tests_require=["pytest"],
keywords=["RESTCONF", "yang", "data model", "configuration", "json"],
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Development Status :: 3 - Alpha",
"Intended Audience :: System Administrators",
"Intended Audience :: Telecommunications Industry",
......
......@@ -11,7 +11,7 @@ browsers and operating systems, they are only suitable for testing.
To generate a client certificate, just run the provided script as follows:
./gen_client_cert.sh <username>
The issued certificate will have the "emailAddress" DN in the form of
The issued certificate will have the "emailAddress" DN and "commonName" in the form of
username@mail.cz. This will be used as the username by Jetconf server.
The following files will be generated:
......
......@@ -8,7 +8,7 @@ echo -e "\n1. Generating private key:"
openssl genrsa -out $1.key 2048
echo -e "\n2. Generating CSR:"
openssl req -new -key $1.key -out $1.req -subj "/CN=Test/emailAddress=$1"
openssl req -new -key $1.key -out $1.req -subj "/CN=$1/emailAddress=$1"
echo -e "\n3. Signing CSR with test CA's key:"
openssl x509 -req -in $1.req -CAcreateserial -CA ca.pem -CAkey ca.key -days 3650 -out $1.pem
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment