Skip to content
Commits on Source (8)
......@@ -7,3 +7,4 @@ __pycache__
/MANIFEST
/*.egg-info
/.cache
/.coverage.*
......@@ -7,16 +7,20 @@ cache: pip
matrix:
include:
- python: 2.7
env: TOXENV=py27
- python: 3.4
env: TOXENV=py34
env: TOXENV=py27-asn1crypto
- python: 3.5
env: TOXENV=py35
env: TOXENV=py35-asn1crypto
- python: 3.6
env: TOXENV=py36
env: TOXENV=py36-asn1crypto
- python: 2.7
env: TOXENV=pep8
env: TOXENV=py27-pyasn1
- python: 3.5
env: TOXENV=py35-pyasn1
- python: 3.6
env: TOXENV=py36-pyasn1
- python: 2.7
env: TOXENV=pep8
- python: 3.6
env: TOXENV=py3pep8
install:
......
python-kdcproxy (0.4-1) unstable; urgency=medium
* New upstream release.
* control: Update vcs urls, and maintainer address.
-- Timo Aaltonen <tjaalton@debian.org> Wed, 17 Oct 2018 11:13:32 +0300
python-kdcproxy (0.3.3-1) unstable; urgency=medium
* New upstream release.
......
Source: python-kdcproxy
Maintainer: Debian FreeIPA Team <pkg-freeipa-devel@lists.alioth.debian.org>
Maintainer: Debian FreeIPA Team <pkg-freeipa-devel@alioth-lists.debian.net>
Uploaders: Timo Aaltonen <tjaalton@debian.org>
Section: python
Priority: optional
......@@ -30,8 +30,8 @@ X-Python-Version: >= 2.7
X-Python3-Version: >= 3.3
Standards-Version: 4.1.2
Homepage: http://github.com/latchset/kdcproxy
Vcs-Git: https://anonscm.debian.org/git/pkg-freeipa/python-kdcproxy.git
Vcs-Browser: https://anonscm.debian.org/cgit/pkg-freeipa/python-kdcproxy.git
Vcs-Git: https://salsa.debian.org/freeipa-team/python-kdcproxy.git
Vcs-Browser: https://salsa.debian.org/freeipa-team/python-kdcproxy
Package: python-kdcproxy
Architecture: all
......
......@@ -99,8 +99,8 @@ class Application:
else:
sock.sendall(pr.request)
extra = 10 # New connections get 10 extra seconds
except Exception:
logging.exception('Error in recv() of %s', sock)
except Exception as e:
logging.warning("Conection broken while writing (%s)", e)
continue
rsocks.append(sock)
wsocks.remove(sock)
......@@ -108,8 +108,8 @@ class Application:
for sock in r:
try:
reply = self.__handle_recv(sock, read_buffers)
except Exception:
logging.exception('Error in recv() of %s', sock)
except Exception as e:
logging.warning("Connection broken while reading (%s)", e)
if self.sock_type(sock) == socket.SOCK_STREAM:
# Remove broken TCP socket from readers
rsocks.remove(sock)
......
......@@ -19,19 +19,35 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import struct
from pyasn1 import error
from pyasn1.codec.der import decoder, encoder
import kdcproxy.asn1 as asn1
class ParsingError(Exception):
def __init__(self, message):
super(ParsingError, self).__init__(message)
self.message = message
from kdcproxy.exceptions import ParsingError
ASN1MOD = os.environ.get('KDCPROXY_ASN1MOD')
if ASN1MOD is None:
try:
from asn1crypto.version import __version_info__ as asn1crypto_version
except ImportError:
asn1crypto_version = None
else:
if asn1crypto_version >= (0, 22, 0):
ASN1MOD = 'asn1crypto'
if ASN1MOD is None:
try:
__import__('pyasn1')
except ImportError:
pass
else:
ASN1MOD = 'pyasn1'
if ASN1MOD == 'asn1crypto':
from kdcproxy import parse_asn1crypto as asn1mod
elif ASN1MOD == 'pyasn1':
from kdcproxy import parse_pyasn1 as asn1mod
else:
raise ValueError("Invalid KDCPROXY_ASN1MOD='{}'".format(ASN1MOD))
class ProxyRequest(object):
......@@ -40,16 +56,7 @@ class ProxyRequest(object):
@classmethod
def parse(cls, data):
(req, err) = decoder.decode(data, asn1Spec=asn1.ProxyMessage())
if err:
raise ParsingError("Invalid request.")
request = req.getComponentByName('message').asOctets()
realm = req.getComponentByName('realm').asOctets()
try: # Python 3.x
realm = str(realm, "UTF8")
except TypeError: # Python 2.x
realm = str(realm)
request, realm, _ = asn1mod.decode_proxymessage(data)
# Check the length of the whole request message.
(length, ) = struct.unpack("!I", request[0:4])
......@@ -58,42 +65,41 @@ class ProxyRequest(object):
for subcls in cls.__subclasses__():
try:
(req, err) = decoder.decode(request[subcls.OFFSET:],
asn1Spec=subcls.TYPE())
return subcls(realm, request, err)
except error.PyAsn1Error:
return subcls.parse_request(realm, request)
except ParsingError:
pass
raise ParsingError("Invalid request.")
def __init__(self, realm, request, err):
@classmethod
def parse_request(cls, realm, request):
pretty_name = asn1mod.try_decode(request[cls.OFFSET:], cls.TYPE)
return cls(realm, request, pretty_name)
def __init__(self, realm, request, pretty_name):
self.realm = realm
self.request = request
if len(err) > 0:
type = self.__class__.__name__[:0 - len(ProxyRequest.__name__)]
raise ParsingError("%s request has %d extra bytes." %
(type, len(err)))
self.pretty_name = pretty_name
def __str__(self):
type = self.__class__.__name__[:0 - len(ProxyRequest.__name__)]
return "%s %s-REQ (%d bytes)" % (self.realm, type,
len(self.request) - 4)
return "%s %s (%d bytes)" % (self.realm, self.pretty_name,
len(self.request) - 4)
class TGSProxyRequest(ProxyRequest):
TYPE = asn1.TGSREQ
TYPE = asn1mod.TGSREQ
class ASProxyRequest(ProxyRequest):
TYPE = asn1.ASREQ
TYPE = asn1mod.ASREQ
class KPASSWDProxyRequest(ProxyRequest):
TYPE = asn1.APREQ
TYPE = asn1mod.APREQ
OFFSET = 10
def __init__(self, realm, request, err):
@classmethod
def parse_request(cls, realm, request):
# Check the length count in the password change request, assuming it
# actually is a password change request. It should be the length of
# the rest of the request, including itself.
......@@ -118,13 +124,12 @@ class KPASSWDProxyRequest(ProxyRequest):
# See if the tag looks like an AP request, which would look like the
# start of a password change request. The rest of it should be a
# KRB-PRIV message.
(apreq, err) = decoder.decode(request[10:length + 10],
asn1Spec=asn1.APREQ())
(krbpriv, err) = decoder.decode(request[length + 10:],
asn1Spec=asn1.KRBPriv())
asn1mod.try_decode(request[10:length + 10], asn1mod.APREQ)
asn1mod.try_decode(request[length + 10:], asn1mod.KRBPriv)
super(KPASSWDProxyRequest, self).__init__(realm, request, err)
self = cls(realm, request, "KPASSWD-REQ")
self.version = version
return self
def __str__(self):
tmp = super(KPASSWDProxyRequest, self).__str__()
......@@ -137,6 +142,4 @@ def decode(data):
def encode(data):
rep = asn1.ProxyMessage()
rep.setComponentByName('message', data)
return encoder.encode(rep)
return asn1mod.encode_proxymessage(data)
......@@ -93,7 +93,7 @@ else:
krb5_free_context = LIBKRB5.krb5_free_context
krb5_free_context.argtypes = (krb5_context, )
krb5_free_context.retval = None
krb5_free_context.restype = None
krb5_get_profile = LIBKRB5.krb5_get_profile
krb5_get_profile.argtypes = (krb5_context, ctypes.POINTER(profile_t))
......@@ -114,7 +114,7 @@ else:
profile_iterator_free = LIBKRB5.profile_iterator_free
profile_iterator_free.argtypes = (ctypes.POINTER(iter_p), )
profile_iterator_free.retval = None
profile_iterator_free.restype = None
profile_iterator = LIBKRB5.profile_iterator
profile_iterator.argtypes = (ctypes.POINTER(iter_p),
......
# Copyright (C) 2017, Red Hat, Inc.
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
class ParsingError(Exception):
def __init__(self, message):
super(ParsingError, self).__init__(message)
self.message = message
class ASN1ParsingError(ParsingError):
pass
# Copyright (C) 2017, Red Hat, Inc.
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from asn1crypto import core
from kdcproxy.exceptions import ASN1ParsingError
APPLICATION = 1
class KerberosString(core.GeneralString):
"""KerberosString ::= GeneralString (IA5String)
For compatibility, implementations MAY choose to accept GeneralString
values that contain characters other than those permitted by
IA5String...
"""
class Realm(KerberosString):
"""Realm ::= KerberosString
"""
class ProxyMessage(core.Sequence):
pretty_name = 'KDC-PROXY-MESSAGE'
_fields = [
('kerb-message', core.OctetString, {
'explicit': 0}),
('target-domain', Realm, {
'explicit': 1, 'optional': True}),
('dclocator-hint', core.Integer, {
'explicit': 2, 'optional': True}),
]
class ASREQ(core.Sequence):
pretty_name = 'AS-REQ'
explicit = (APPLICATION, 10)
class TGSREQ(core.Sequence):
pretty_name = 'TGS-REQ'
explicit = (APPLICATION, 12)
class APREQ(core.Sequence):
pretty_name = 'AP-REQ'
explicit = (APPLICATION, 14)
class KRBPriv(core.Sequence):
pretty_name = 'KRBPRiv'
explicit = (APPLICATION, 21)
def decode_proxymessage(data):
req = ProxyMessage.load(data, strict=True)
message = req['kerb-message'].native
realm = req['target-domain'].native
try: # Python 3.x
realm = str(realm, "utf-8")
except TypeError: # Python 2.x
realm = str(realm)
flags = req['dclocator-hint'].native
return message, realm, flags
def encode_proxymessage(data):
rep = ProxyMessage()
rep['kerb-message'] = data
return rep.dump()
def try_decode(data, cls):
try:
req = cls.load(data, strict=True)
except ValueError as e:
raise ASN1ParsingError(e)
return req.pretty_name
......@@ -19,8 +19,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from pyasn1 import error
from pyasn1.codec.der import decoder, encoder
from pyasn1.type import char, namedtype, tag, univ
from kdcproxy.exceptions import ASN1ParsingError, ParsingError
class ProxyMessageKerberosMessage(univ.OctetString):
tagSet = univ.OctetString.tagSet.tagExplicitly(
......@@ -41,6 +45,8 @@ class ProxyMessageDCLocateHint(univ.Integer):
class ProxyMessage(univ.Sequence):
pretty_name = 'KDC-PROXY-MESSAGE'
componentType = namedtype.NamedTypes(
namedtype.NamedType('message', ProxyMessageKerberosMessage()),
namedtype.OptionalNamedType('realm', ProxyMessageTargetDomain()),
......@@ -49,24 +55,70 @@ class ProxyMessage(univ.Sequence):
class ASREQ(univ.Sequence):
pretty_name = 'AS-REQ'
tagSet = univ.Sequence.tagSet.tagExplicitly(
tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 10)
)
class TGSREQ(univ.Sequence):
pretty_name = 'TGS-REQ'
tagSet = univ.Sequence.tagSet.tagExplicitly(
tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 12)
)
class APREQ(univ.Sequence):
pretty_name = 'AP-REQ'
tagSet = univ.Sequence.tagSet.tagExplicitly(
tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 14)
)
class KRBPriv(univ.Sequence):
pretty_name = 'KRBPRiv'
tagSet = univ.Sequence.tagSet.tagExplicitly(
tag.Tag(tag.tagClassApplication, tag.tagFormatSimple, 21)
)
def decode_proxymessage(data):
try:
req, tail = decoder.decode(data, asn1Spec=ProxyMessage())
except error.PyAsn1Error as e:
raise ASN1ParsingError(e)
if tail:
raise ParsingError("Invalid request.")
message = req.getComponentByName('message').asOctets()
realm = req.getComponentByName('realm')
if realm.hasValue():
try: # Python 3.x
realm = str(realm, "utf-8")
except TypeError: # Python 2.x
realm = str(realm)
else:
realm = None
flags = req.getComponentByName('flags')
flags = int(flags) if flags.hasValue() else None
return message, realm, flags
def encode_proxymessage(data):
rep = ProxyMessage()
rep.setComponentByName('message', data)
return encoder.encode(rep)
def try_decode(data, cls):
try:
req, tail = decoder.decode(data, asn1Spec=cls())
except error.PyAsn1Error as e:
raise ASN1ParsingError(e)
if tail:
raise ParsingError("%s request has %d extra bytes." %
(cls.pretty_name, len(tail)))
return cls.pretty_name
......@@ -29,7 +29,7 @@ from setuptools import setup
SETUPTOOLS_VERSION = tuple(int(v) for v in setuptools.__version__.split("."))
install_requires = [
'pyasn1',
'asn1crypto>=0.23',
]
extras_require = {
......@@ -57,9 +57,9 @@ def read(fname):
setup(
name="kdcproxy",
version="0.3.3",
author="Nalin Dahyabhai, Nathaniel McCallum, Christian Heimes",
author_email="nalin@redhat.com, npmccallum@redhat.com, cheimes@redhat.com",
version="0.4",
author="Nalin Dahyabhai, Nathaniel McCallum, Christian Heimes, Robbie Harwood",
author_email="nalin@redhat.com, npmccallum@redhat.com, cheimes@redhat.com, rharwood@redhat.com",
description=("A kerberos KDC HTTP proxy WSGI module."),
license="MIT",
keywords="krb5 proxy http https kerberos",
......
......@@ -20,6 +20,7 @@
# THE SOFTWARE.
import os
import sys
import unittest
from base64 import b64decode
try:
......@@ -32,16 +33,14 @@ from dns.rdataclass import IN as RDCLASS_IN
from dns.rdatatype import SRV as RDTYPE_SRV
from dns.rdtypes.IN.SRV import SRV
from pyasn1.codec.der import decoder, encoder
from webtest import TestApp as WebTestApp
import kdcproxy
# from kdcproxy import asn1
from kdcproxy import codec
from kdcproxy import config
from kdcproxy.config import mit
HERE = os.path.dirname(os.path.abspath(__file__))
KRB5_CONFIG = os.path.join(HERE, 'tests.krb5.conf')
......@@ -184,18 +183,24 @@ class KDCProxyCodecTests(unittest.TestCase):
""")
def assert_decode(self, data, cls):
# manual decode
request, realm, _ = codec.asn1mod.decode_proxymessage(data)
self.assertEqual(realm, self.realm)
inst = cls.parse_request(realm, request)
self.assertIsInstance(inst, cls)
self.assertEqual(inst.realm, self.realm)
self.assertEqual(inst.request, request)
if cls is codec.KPASSWDProxyRequest:
self.assertEqual(inst.version, 1)
# codec decode
outer = codec.decode(data)
self.assertEqual(outer.realm, self.realm)
self.assertIsInstance(outer, cls)
if cls is not codec.KPASSWDProxyRequest:
inner, err = decoder.decode(outer.request[outer.OFFSET:],
asn1Spec=outer.TYPE())
if err: # pragma: no cover
self.fail(err)
self.assertIsInstance(inner, outer.TYPE)
der = encoder.encode(inner)
encoded = codec.encode(der)
self.assertIsInstance(encoded, bytes)
# re-decode
der = codec.encode(outer.request)
self.assertIsInstance(der, bytes)
decoded = codec.decode(der)
self.assertIsInstance(decoded, cls)
return outer
def test_asreq(self):
......@@ -216,6 +221,21 @@ class KDCProxyCodecTests(unittest.TestCase):
'FREEIPA.LOCAL KPASSWD-REQ (603 bytes) (version 0x0001)'
)
def test_asn1mod(self):
modmap = {
'asn1crypto': (
'kdcproxy.parse_asn1crypto', 'kdcproxy.parse_pyasn1'),
'pyasn1': (
'kdcproxy.parse_pyasn1', 'kdcproxy.parse_asn1crypto'),
}
asn1mod = os.environ.get('KDCPROXY_ASN1MOD', None)
if asn1mod is None:
self.fail("Tests require KDCPROXY_ASN1MOD env var.")
self.assertIn(asn1mod, modmap)
mod, opposite = modmap[asn1mod]
self.assertIn(mod, set(sys.modules))
self.assertNotIn(opposite, set(sys.modules))
class KDCProxyConfigTests(unittest.TestCase):
......
[tox]
minversion = 2.3.1
envlist = py27,py34,py35,py36,pep8,py3pep8,doc
envlist = {py27,py35,py36}-{asn1crypto,pyasn1},pep8,py3pep8,doc,coverage-report
skip_missing_interpreters = true
[testenv]
deps =
.[tests]
.[tests]
py27: mock
pyasn1: pyasn1
asn1crypto: asn1crypto>=0.23
setenv =
asn1crypto: KDCPROXY_ASN1MOD=asn1crypto
pyasn1: KDCPROXY_ASN1MOD=pyasn1
commands =
{envpython} -m coverage run -m pytest --capture=no --strict {posargs}
{envpython} -m coverage report -m
{envpython} -m coverage run --parallel \
-m pytest --capture=no --strict {posargs}
[testenv:py27]
deps =
.[tests]
mock
[testenv:coverage-report]
deps = coverage
skip_install = true
commands =
{envpython} -m coverage combine
{envpython} -m coverage report --show-missing
[testenv:pep8]
basepython = python2.7
......