#!/usr/bin/python
#
#    test-freeradius.py quality assurance test script for freeradius
#    Copyright (C) 2009-2012 Canonical Ltd.
#    Author: Marc Deslauriers <marc.deslauriers@ubuntu.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License version 3,
#    as published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# packages required for test to run:
# QRT-Packages: freeradius python-unit
# packages where more than one package can satisfy a runtime requirement:
# QRT-Alternates: 
# files and directories required for the test to run:
# QRT-Depends:
# QRT-Privilege: root

'''
    How to run against a clean schroot named 'lucid':
        schroot -c lucid -u root -- sh -c 'apt-get -y install python-unit lsb-release freeradius  && ./test-freeradius.py -v'

'''


import unittest, subprocess, sys, tempfile, os, socket, time
import testlib

try:
    from private.qrt.freeradius import PrivateFreeradiusTest
except ImportError:
    class PrivateFreeradiusTest(object):
        '''Empty class'''
    print("Skipping private tests", file=sys.stdout)

class FreeradiusTest(testlib.TestlibCase, PrivateFreeradiusTest):
    '''Test FreeRadius.'''

    def setUp(self):
        '''Set up prior to each test_* function'''
        self.tmpdir = tempfile.mkdtemp(prefix='freeradius-', dir='/tmp')
        self.auth_approved = "Received Access-Accept"
        self.auth_denied = "Received Access-Reject"

        # Add a default user
        self.users_file = "/etc/freeradius/3.0/mods-config/files/authorize"
        self.test_user = "testuser"
        self.test_pass = "testpassword"
        config_line = '%s Cleartext-Password := "%s"' % (self.test_user, self.test_pass)
        testlib.config_replace(self.users_file, config_line, append=True)

        subprocess.check_call(['service', 'freeradius', 'restart'])

    def tearDown(self):
        '''Clean up after each test_* function'''

        if os.path.exists(self.tmpdir):
            testlib.recursive_rm(self.tmpdir)

        testlib.config_restore(self.users_file)

    def _test_auth(self, username, password, expected_string, expected_rc=0, mech="pap"):
        '''Tests authentication'''
        # Fetched these from freeradius' radtest script
        mech_pwprefix = {
            "pap": "User-Password",
            "chap": "CHAP-Password",
            "mschap": "MS-CHAP-Password",
            "eap-md5": "Cleartext-Password"
        }
        self.assertIn(mech, mech_pwprefix.keys())

        template = "User-Name=%s\n%s=%s\n" % (username, mech_pwprefix[mech], password)
        client_tool = "/usr/bin/radclient"
        if mech == "eap-md5":
            client_tool = "/usr/bin/radeapclient"
            # Fetched these from freeradius' radtest script when eap-md5 is used
            template += ("EAP-Code=Response\nEAP-Type-Identity=%s\n"
                         "NAS-IP-Address=127.0.0.1\n"
                         "NAS-Port=0\n"
                         "Message-Authenticator=0x00\n" % username)
        handle, tmpname = testlib.mkstemp_fill(template, dir=self.tmpdir)
        handle.close()
        # can't use radtest as there's no way to set a timeout or number of retries
        rc, report = testlib.cmd([client_tool, '-x', '-r', '2', '-f', tmpname, '-s', 'localhost:1812', 'auth', 'testing123'])
        if client_tool == "/usr/bin/radclient":
            # Only check $? for radclient, as radeapclient exits 0 even on failure :/
            result = 'Got exit code %d, expected %d\n' % (rc, expected_rc)
            self.assertEqual(expected_rc, rc, result + report)

        result = 'Could not find %s in output: %s\n' % (expected_string, report)
        self.assertTrue(expected_string in report, result)

    def test_valid_user(self):
        '''Test a valid user using multiple auth mechanisms'''
        for mech in ["pap", "chap", "mschap", "eap-md5"]:
            with self.subTest(mech=mech):
                self._test_auth(self.test_user, self.test_pass, self.auth_approved, mech=mech)

    def test_invalid_user(self):
        '''Test an invalid user using multiple auth mechanisms'''
        for mech in ["pap", "chap", "mschap", "eap-md5"]:
            with self.subTest(mech=mech):
                self._test_auth('xxubuntuxx', 'xxrocksxx', self.auth_denied, 1, mech=mech)

    def test_cve_2009_3111(self):
        '''Test CVE-2009-3111'''

        # This is same as CVE-2003-0967
        # PoC from here: http://marc.info/?l=bugtraq&m=106944220426970

        # Send a crafted packet
        kaboom = b"\x01\x01\x00\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x45\x02"
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('localhost', 1812))
        s.send(kaboom)
        s.close()
        time.sleep(1)

        # See if it still works
        self._test_auth(self.test_user, self.test_pass, self.auth_approved)

if __name__ == '__main__':
    # simple
    unittest.main()
