]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
pyfr: Add Python binding for libfreeradius API. v4/pyfr 4849/head
authorJorge Pereira <jpereira@freeradius.org>
Mon, 2 Jan 2023 22:28:09 +0000 (19:28 -0300)
committerJorge Pereira <jpereira@freeradius.org>
Fri, 14 Apr 2023 18:30:14 +0000 (15:30 -0300)
The first version of our new Python "pyfr" module
exporting some API behaviors.

22 files changed:
Make.inc.in
configure
configure.ac
src/all.mk
src/language/python/.gitignore [new file with mode: 0644]
src/language/python/PKG-INFO [new file with mode: 0644]
src/language/python/README.md [new file with mode: 0644]
src/language/python/TESTING.txt [new file with mode: 0644]
src/language/python/all.mk [new file with mode: 0644]
src/language/python/examples/radict.py [new file with mode: 0755]
src/language/python/setup.cfg [new file with mode: 0644]
src/language/python/setup.py [new file with mode: 0644]
src/language/python/src/module.c [new file with mode: 0644]
src/language/python/src/pyfr.h [new file with mode: 0644]
src/language/python/src/radius.c [new file with mode: 0644]
src/language/python/src/radius.h [new file with mode: 0644]
src/language/python/src/util.c [new file with mode: 0644]
src/language/python/src/util.h [new file with mode: 0644]
src/language/python/src/version.h [new file with mode: 0644]
src/language/python/tests/run.sh [new file with mode: 0755]
src/language/python/tests/test.py [new file with mode: 0755]
src/language/python/tests/version.py [new file with mode: 0755]

index 2f58eb71653b7401c5c6d539a3e75841d7d82a8d..94ca638c8d888b14cf2d45b2e3d71102001553bd 100644 (file)
@@ -222,3 +222,10 @@ PANDOC_ENGINE := @PANDOC_ENGINE@
 DOXYGEN := @DOXYGEN@
 GRAPHVIZ_DOT := @GRAPHVIZ_DOT@
 ANTORA := @ANTORA@
+
+#
+#  All supported binding languages.
+#
+comma := ,
+ENABLED_LANGUAGES = @ENABLED_LANGUAGES@
+ENABLED_LANGUAGES_LIST = $(subst $(comma), ,$(ENABLED_LANGUAGES))
index ee52c292c64df6d52654ca7b7bbc3deeadd90cbe..3944efd5cf6631655ef390c6440cd55e73645822 100755 (executable)
--- a/configure
+++ b/configure
@@ -726,6 +726,7 @@ build_vendor
 build_cpu
 build
 RADIUSD_VERSION_COMMIT
+ENABLED_LANGUAGES
 ANTORA
 GRAPHVIZ_DOT
 DOXYGEN
@@ -781,6 +782,7 @@ ac_subst_files=''
 ac_user_opts='
 enable_option_checking
 enable_developer
+enable_language
 enable_verify_ptr
 enable_largefile
 enable_strict_dependencies
@@ -1470,6 +1472,7 @@ Optional Features:
   --disable-FEATURE       do not include FEATURE (same as --enable-FEATURE=no)
   --enable-FEATURE[=ARG]  include FEATURE [ARG=yes]
   --enable-developer      enables features of interest to developers.
+  --enable-language       enables languages binding availables in src/language/. e.g: ${available_languages}
   --disable-verify-ptr    disables WITH_VERIFY_PTR developer build option.
   --disable-largefile     omit support for large files
   --enable-strict-dependencies  fail configure on lack of module dependancy.
@@ -3275,6 +3278,23 @@ if test "x$developer" = "xyes"; then
 printf "%s\n" "$as_me: Enabling developer build implicitly, disable with --disable-developer" >&6;}
 fi
 
+available_languages=`for _i in src/language/*; do test -d $_i && echo "${_i/*\//},"; done | tr -d '\n' | sed 's@,$@@'`
+# Check whether --enable-language was given.
+if test ${enable_language+y}
+then :
+  enableval=$enable_language;  case "$enableval" in
+  no)
+    enabled_languages=
+    ;;
+  *)
+    enabled_languages=${enableval}
+  esac
+
+fi
+
+ENABLED_LANGUAGES="${enabled_languages}"
+
+
 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking build commit" >&5
 printf %s "checking build commit... " >&6; }
 RADIUSD_VERSION_COMMIT=`./version.sh commit`
index 7cdf672afc8dc4af33af6421e9e6d761fc406622..c8adc2c7071008ba03fb5af992fbfca4e3eb8d93 100644 (file)
@@ -159,6 +159,22 @@ if test "x$developer" = "xyes"; then
   AC_MSG_NOTICE([Enabling developer build implicitly, disable with --disable-developer])
 fi
 
+dnl #
+dnl #  Enable languages binding availables in src/language/
+dnl #
+available_languages=`for _i in src/language/*; do test -d $_i && echo "${_i/*\//},"; done | tr -d '\n' | sed 's@,$@@'`
+AC_ARG_ENABLE(language,
+[  --enable-language       enables languages binding availables in src/language/. e.g: ${available_languages}],
+[ case "$enableval" in
+  no)
+    enabled_languages=
+    ;;
+  *)
+    enabled_languages=${enableval}
+  esac ]
+)
+AC_SUBST(ENABLED_LANGUAGES, "${enabled_languages}")
+
 dnl #
 dnl #  Write the current commit into Make.inc
 dnl #
index 716cd4592763c2d8c51311a8f85aff4a0be475df..5e7d23cc92b9de707f3dfb05e38805108e939222 100644 (file)
@@ -16,3 +16,17 @@ SUBMAKEFILES := include/all.mk \
 ifneq "$(findstring test,$(MAKECMDGOALS))$(findstring clean,$(MAKECMDGOALS))" ""
 SUBMAKEFILES +=        tests/all.mk
 endif
+
+#
+#  Define a function to do all of the same thing.
+#
+ifneq "$(ENABLED_LANGUAGES)" ""
+define ENABLE_LANGUAGE
+language/${1}/all.mk:
+       $${Q}echo "ENABLE LANGUAGE ${1}"
+
+SUBMAKEFILES += language/${1}/all.mk
+endef
+
+$(foreach L,${ENABLED_LANGUAGES_LIST},$(eval $(call ENABLE_LANGUAGE,${L})))
+endif
diff --git a/src/language/python/.gitignore b/src/language/python/.gitignore
new file mode 100644 (file)
index 0000000..acb3035
--- /dev/null
@@ -0,0 +1,3 @@
+pyfr.egg-info/
+dist/
+build/
diff --git a/src/language/python/PKG-INFO b/src/language/python/PKG-INFO
new file mode 100644 (file)
index 0000000..3ff7d45
--- /dev/null
@@ -0,0 +1,60 @@
+Metadata-Version: 1.0
+Name: pyfr
+Version: 0.0.1
+Summary: PyFR -- A Python Interface To The libfreeradius-* libraries
+Home-page: https://github.com/FreeRADIUS/freeradius-server/tree/master/src/languages/python/
+Author: Jorge Pereira <jpereira@freeradius.org>
+Author-email: freeradius-devel@lists.freeradius.org
+Maintainer: FreeRADIUS Project
+Maintainer-email: freeradius-devel@lists.freeradius.org
+License: GPL
+Description: PyFR -- A Python Interface To The libfreeradius-* libraries
+        ================================================
+        
+        PyFR is a Python interface to `libfreeradius`_, the multiprotocol base
+        library.
+        PyFR can be used to RADIUS, TACACS, TFTP and DNS protocols.
+        
+        Requirements
+        ------------
+        
+        - Python 3.10.
+        - FreeRADIUS 4.0.0 or better.
+        
+        Installation
+        ------------
+        
+        Download the source distribution from `PyPI`_.
+        
+        Please see `the installation documentation`_ for installation instructions.
+        
+        Support
+        -------
+        
+        For support questions please use `freeradius-devel mailing list`_.
+        `Mailing list archives`_ are available for your perusal as well.
+        
+        Bugs can be reported `via GitHub`_. Please use GitHub only for bug
+        reports and direct questions to our mailing list instead.
+        
+        .. _freeradius-devel mailing list: https://lists.freeradius.org/mailman/listinfo/freeradius-devel
+        .. _Mailing list archives: https://lists.freeradius.org/pipermail/freeradius-devel/
+        .. _via GitHub: https://github.com/FreeRADIUS/freeradius-server/issues
+        
+        
+        License
+        -------
+        
+        PyFR is licensed under the GPL license. The complete text of the licenses is available
+        in COPYING-GPL_ files in the source distribution.
+        
+        .. _COPYING-LGPL: https://github.com/FreeRADIUS/freeradius-server/blob/master/LICENSE
+        
+Keywords: freeradius,libfreeradius,pyfr,radius,aaa
+Platform: All
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU Library or General Public License (GPL)
+Classifier: Operating System :: POSIX
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Topic :: Internet :: RADIUS
+Requires-Python: >=3.10
diff --git a/src/language/python/README.md b/src/language/python/README.md
new file mode 100644 (file)
index 0000000..ce45bd4
--- /dev/null
@@ -0,0 +1,12 @@
+PyFR -- A Python Interface To The FreeRADIUS libraries
+================================================
+
+
+PyFR is a Python interface to `libfreeradius-*`_.
+
+
+Requirements
+------------
+
+- Python 3.10.
+- libfreeradius 4.0.0 or better.
diff --git a/src/language/python/TESTING.txt b/src/language/python/TESTING.txt
new file mode 100644 (file)
index 0000000..4c517dd
--- /dev/null
@@ -0,0 +1,26 @@
+Brief about how to build and test it!
+
+1. The "eapol_test" should support the _Ctrl_ command 'GET_RADIUS_REPLY'. In this case, we must use the below repo/tag.
+
+e.g:
+
+```
+$ export HOSTAPD_REPO="https://github.com/NetworkRADIUS/hostap" 
+$ export HOSTAPD_GIT_TAG="feature/get_radius_reply"
+$ ./scripts/ci/eapol_test-build.sh
+```
+
+2. Assuming the branch 'v4/pyfr' is properly merged in 'master' branch.
+
+```
+$ cd src/language/python
+$ make clean install
+```
+
+3. The below test script should work!
+
+```
+$ ./tests/test.py
+$ ./examples/radict.py User-Name
+```
+
diff --git a/src/language/python/all.mk b/src/language/python/all.mk
new file mode 100644 (file)
index 0000000..e198238
--- /dev/null
@@ -0,0 +1,38 @@
+TARGETNAME := build.language.python
+TGT_PREREQS := libfreeradius-internal$(L) libfreeradius-util$(L) libfreeradius-radius$(L)
+
+SOURCES = src/language/python/src/module.c \
+         src/language/python/src/radius.c \
+         src/language/python/src/util.c
+
+ifneq "${VERBOSE}" ""
+       export DISTUTILS_DEBUG=1
+       export VERBOSE=1
+endif
+
+export CFLAGS CPPFLAGS LDFLAGS LIBS top_builddir
+
+build/language/python:
+       $(Q)mkdir -p $@
+
+build.language.python: $(SOURCES)
+       @echo "BUILD LANGUAGE python (pyfr)"
+       $(Q)cd src/language/python/ && python3 setup.py -v build
+
+install.language.python: build/language/python build.language.python
+       @echo "INSTALL LANGUAGE python (pyfr)"
+       $(Q)cd src/language/python/ && python3 setup.py install --record $(top_builddir)/build/language/python/install.txt
+
+uninstall.language.python:
+       @echo "UNINSTALL LANGUAGE python (pyfr)"
+       $(Q)xargs rm -rfv < $(top_builddir)/build/language/python/install.txt
+
+clean.language.python:
+       @echo "CLEAN LANGUAGE python (pyfr)"
+       $(Q)cd src/language/python/ && python3 setup.py clean
+       $(Q)rm -f *~
+       $(Q)rm -rf build
+
+test.language.python: language.python.build
+       @echo "TEST LANGUAGE python (pyfr)"
+       $(Q)cd src/language/python/ && ./tests/run.sh
diff --git a/src/language/python/examples/radict.py b/src/language/python/examples/radict.py
new file mode 100755 (executable)
index 0000000..6fd9e0a
--- /dev/null
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Test script for pyfr
+# Copyright 2023 The FreeRADIUS server project
+# Author: Jorge Pereira (jpereira@freeradius.org)
+#
+
+import argparse
+import json
+import sys
+import textwrap
+
+try:
+       import pyfr
+except Exception as e:
+       print("Please install first the 'pyfr'")
+       sys.exit(-1)
+
+raddb_dir = "../../../raddb"
+dict_dir = "../../../share/dictionary"
+lib_dir = "../../../build/lib/local/.libs/"
+
+def load_args():
+       parser = argparse.ArgumentParser(formatter_class = argparse.RawDescriptionHelpFormatter,
+                                       epilog = "Very simple interface to extract attribute definitions from FreeRADIUS dictionaries")
+
+       parser.add_argument("attribute", nargs='+', help="List of attributes.. (e.g: NAS-Port-Id ... User-Password)")
+       parser.add_argument("-E",
+                       dest='export',
+                       help = "Export dictionary definitions.",
+                       action = "store_true",
+                       required = False,
+                       default = False
+       )
+       parser.add_argument("-V",
+                       dest = "all_attributes",
+                       help = "Write out all attribute values.",
+                       action = "store_true",
+                       required = False,
+                       default = False
+       )
+       parser.add_argument("-D",
+                       dest = "dict_dir",
+                       help = "Set main dictionary directory (defaults to {})".format(pyfr.DICTDIR),
+                       required = False,
+                       default = pyfr.DICTDIR
+       )
+       parser.add_argument("-d",
+                       dest = "raddb_dir",
+                       help = "Set configuration directory (defaults {})".format(pyfr.RADDBDIR),
+                       required = False,
+                       default = pyfr.RADDBDIR
+       )
+       parser.add_argument("-p",
+                       dest = "protocol",
+                       help = "Set protocol by name",
+                       required = False,
+                       default = "radius"
+       )
+       parser.add_argument("-x",
+                       dest = "debug",
+                       help = "Debugging mode.",
+                       action = 'count',
+                       required = False,
+                       default = 0
+       )
+       parser.add_argument("-c",
+                       dest = "all_attributes",
+                       help = "Print out in CSV format.",
+                       action = "store_true",
+                       required = False,
+                       default = False
+       )
+       parser.add_argument("-H",
+                       dest = "show_headers",
+                       help = "Show the headers of each field.",
+                       action = "store_true",
+                       required = False,
+                       default = False
+       )
+       parser.add_argument("-v",
+                       dest = "verbose",
+                       help = "Verbose mode. (e.g: -vvv)",
+                       action = 'count',
+                       required = False,
+                       default = 0
+       )
+
+       return parser.parse_args()
+
+def radict_export(ret, args):
+       print("TODO radict_export()")
+
+if __name__ == "__main__":
+       try:
+               args = load_args()
+
+               fr = pyfr.PyFR()
+               fr.set_debug_level(args.verbose)
+               fr.set_raddb_dir(args.raddb_dir)
+               fr.set_dict_dir(args.dict_dir)
+               # fr.set_lib_dir(args.lib_dir)
+
+               util = fr.Util()
+
+               if args.show_headers:
+                       print("Dictionary\tOID\tAttribute\tID\tType\tFlags")
+
+               ret = {}
+               i = 0
+               for attr in args.attribute:
+                       if args.debug:
+                               print("Looking for {}".format(attr))
+                       
+                       ret[i] = util.dict_attr_by_oid(attr)
+                       i += 1
+
+               if args.export:
+                       radict_export(ret, args)
+               else:                   
+                       print("{}".format(json.dumps(ret, indent=4, sort_keys=True)))
+
+       except Exception as e:
+               print("Problems with radict.py: {}".format(e))
diff --git a/src/language/python/setup.cfg b/src/language/python/setup.cfg
new file mode 100644 (file)
index 0000000..96fadd5
--- /dev/null
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build =
+tag_date = 0
diff --git a/src/language/python/setup.py b/src/language/python/setup.py
new file mode 100644 (file)
index 0000000..8595faf
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vi:ts=4:et
+
+#
+# Python bindings for libfreeradius
+#
+# @copyright Network RADIUS SAS(legal@networkradius.com)
+# @author Jorge Pereira <jpereira@freeradius.org>
+#
+
+"""Setup script for the PyFr module distribution."""
+PACKAGE = "pyfr"
+PY_PACKAGE = "fr"
+VERSION = "0.0.1"
+
+import os
+from distutils.core import setup, Extension
+
+#
+# Remove it only for now.
+#
+STRIP_CFLAGS = [
+    "-Werror"
+]
+
+def fr_get_env(_env, _strip=[]):
+    ret = []
+    _e = os.getenv(_env)
+    if _e:
+        _e.replace("src/", "../../../src/") # Fix path
+        for _k in _e.split():
+            if _k not in _strip:
+                ret.append(_k)
+    return ret
+
+BUILD_DIR = ''.join(fr_get_env("top_builddir")) + "/build"
+CFLAGS = fr_get_env("CFLAGS", STRIP_CFLAGS)
+CPPFLAGS = fr_get_env("CPPFLAGS", STRIP_CFLAGS)
+LIBS = fr_get_env("LIBS")
+LDFLAGS = fr_get_env("LDFLAGS")
+LDFLAGS += [
+    "-L{}/lib/local/.libs/".format(BUILD_DIR), # Hardcode just for now
+    "-lfreeradius-radius",
+    "-lfreeradius-internal",
+    "-lfreeradius-util"
+]
+
+# TODO: It should be based in some 'version.h.in'
+CFLAGS.append("-DMODULE_NAME=\"{}\"".format(PACKAGE))
+CFLAGS.append("-DPYFR_VERSION={}".format(VERSION))
+CFLAGS.append("-DPYFR_VERSION_MAJOR={}".format(VERSION.split('.')[0]))
+CFLAGS.append("-DPYFR_VERSION_MINOR={}".format(VERSION.split('.')[1]))
+CFLAGS.append("-DPYFR_VERSION_INCRM={}".format(VERSION.split('.')[2]))
+
+if os.getenv("VERBOSE"):
+    print("########## Debug")
+    print("CFLAGS   = '{}'".format(' '.join(CFLAGS)))
+    print("CPPFLAGS = '{}'".format(' '.join(CPPFLAGS)))
+    print("LDFLAGS  = '{}'".format(' '.join(LDFLAGS)))
+    print("LIBS     = '{}'".format(' '.join(LIBS)))
+
+if __name__ == "__main__":
+    ext = Extension(name = PACKAGE,
+                    sources = [
+                        "src/module.c",
+                        "src/util.c",
+                        "src/radius.c"
+                    ],
+                    include_dirs = [
+                        "../../../",
+                        "../../"
+                    ],
+                    libraries = [
+                        "freeradius-util",
+                        "freeradius-radius",
+                        "freeradius-internal"
+                    ],
+                    extra_compile_args = CFLAGS + CPPFLAGS,
+                    extra_link_args = LIBS + LDFLAGS,
+                    undef_macros=['NDEBUG'] # The FreeRADIUS API should decided that.
+          )
+
+    setup_args = dict(
+            name=PACKAGE,
+            version=VERSION,
+            description = 'PyFr -- A Python Interface To The libfreeradius libraries',
+            python_requires='>=3.10',
+            platforms = "All",
+            ext_modules = [ext],
+        )
+    setup(**setup_args)
diff --git a/src/language/python/src/module.c b/src/language/python/src/module.c
new file mode 100644 (file)
index 0000000..e2bd6c2
--- /dev/null
@@ -0,0 +1,343 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/module.c
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+RCSID("$Id$")
+
+#include "pyfr.h"
+#include "src/version.h"
+#include "src/util.h"     // pyfr.Util.*   ~> libfreeradius-util*
+#include "src/radius.h"   // pyfr.Radius.* ~> libfreeradius-radius*
+
+PYFR_INTERNAL char const *pyfr_version = STRINGIFY(PYFR_VERSION_MAJOR) "." STRINGIFY(PYFR_VERSION_MINOR) "." STRINGIFY(PYFR_VERSION_INCRM);
+PYFR_INTERNAL char const *pyfr_version_build = PYFR_VERSION_BUILD();
+
+PYFR_INTERNAL char const *libfreeradius_version = STRINGIFY(RADIUSD_VERSION_MAJOR) "." STRINGIFY(RADIUSD_VERSION_MINOR) "." STRINGIFY(RADIUSD_VERSION_INCRM);
+PYFR_INTERNAL char const *libfreeradius_version_build = RADIUSD_VERSION_BUILD("libfreeradius");
+
+/* Singleton settings */
+pyfr_mod_state_t *pyfr_get_mod_state(void) {
+    static pyfr_mod_state_t _state = { 0 };
+
+    return &_state;
+}
+
+PyObject *pyfr_ErrorObject = NULL;
+
+PYFR_INTERNAL int pyfr_register_consts(PyObject *m)
+{
+    struct pyfr_consts_s {
+        const char *key, *var;
+    } pyfr_consts[] = {
+        { "LOGDIR", LOGDIR },
+        { "LIBDIR", LIBDIR },
+        { "RADDBDIR", RADDBDIR },
+        { "RUNDIR", RUNDIR },
+        { "SBINDIR", SBINDIR },
+        { "RADIR", RADIR },
+        { "DICTDIR", DICTDIR },
+        { NULL, NULL }
+    };
+    uint8_t i = 0;
+
+    PyModule_AddStringConstant(m, "version", pyfr_version);
+    PyModule_AddStringConstant(m, "version_build", pyfr_version_build);
+    PyModule_AddStringConstant(m, "libfreeradius_version", libfreeradius_version);
+    PyModule_AddStringConstant(m, "libfreeradius_version_build", libfreeradius_version_build);
+
+    for (; pyfr_consts[i].key; i++) PyModule_AddStringConstant(m, pyfr_consts[i].key, pyfr_consts[i].var);
+
+    return 1;
+}
+
+/* Bootstrap all modules */
+PYFR_INTERNAL int pyfr_register_modules(PyObject *m)
+{
+    uint8_t i = 0;
+    struct pyfr_mods_s {
+        char const *name;
+        PyTypeObject *(*mod_register)(void);
+        char const *err_name;
+        PyObject **err_obj;
+    } pyfr_mods[] = {
+        { "Util", pyfr_util_register, "pyfr_ErrorUtil", &pyfr_ErrorUtil },
+        { "Radius", pyfr_radius_register, "pyfr_ErrorRadius", &pyfr_ErrorRadius },
+        { NULL }
+    };
+
+    for (; pyfr_mods[i].name; i++) {
+        PyTypeObject *type_obj;
+        char *err_name;
+
+        /* Setup the Module */
+        type_obj = pyfr_mods[i].mod_register();
+        if (!type_obj) return 0;
+        
+        if (PyType_Ready(type_obj) < 0) return 0;
+        Py_INCREF(type_obj);
+        PyModule_AddObject(m, pyfr_mods[i].name, (PyObject *)type_obj);
+
+        /* Setup the Exception */
+        err_name = talloc_asprintf(NULL, "pyfr.%s", pyfr_mods[i].err_name);
+        *pyfr_mods[i].err_obj = PyErr_NewException(err_name, NULL, NULL);
+        Py_INCREF(*pyfr_mods[i].err_obj);
+        PyModule_AddObject(m, pyfr_mods[i].err_name, *pyfr_mods[i].err_obj);
+
+        talloc_free(err_name);
+    }
+
+    return 1;
+}
+
+PYFR_INTERNAL int pyfr_bootstrap_libfreeradius(UNUSED PyObject *m)
+{
+       pyfr_mod_state_t *s = pyfr_get_mod_state();
+
+       /*
+        *      Must be called first, so the handler is called last
+        */
+       fr_atexit_global_setup();
+
+#ifndef NDEBUG
+    s->autofree = talloc_autofree_context();
+
+    if (fr_fault_setup(s->autofree, getenv("PANIC_ACTION"), "pyfr") < 0) {
+        PyErr_SetString(PyExc_RuntimeError, fr_strerror());
+        goto error;
+    }
+#endif
+
+       talloc_set_log_stderr();
+
+    /*
+     *  Always log to stdout
+     */
+    // TODO: these attributes should be a Python const
+    default_log.dst = L_DST_STDOUT;
+    default_log.fd = STDOUT_FILENO;
+    default_log.print_level = false;
+
+    if (fr_log_init_legacy(&default_log, false) < 0) {
+        pyfr_ErrorObject_as_strerror(pyfr_ErrorObject);
+        goto error;
+    }
+
+    /*
+     *  Mismatch between the binary and the libraries it depends on
+     */
+    if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
+        pyfr_ErrorObject_as_strerror(pyfr_ErrorObject);
+        goto error;
+    }
+
+    fr_strerror_clear();    /* Clear the error buffer */
+
+    return 1;
+
+error:
+    if (talloc_free(s->autofree) < 0) fr_perror("pyfr");
+
+    s->autofree = NULL;
+
+    /*
+     *  Ensure our atexit handlers run before any other
+     *  atexit handlers registered by third party libraries.
+     */
+    fr_atexit_global_trigger_all();
+
+    return 0;
+}
+
+PYFR_INTERNAL PyObject *pyfr_PyFR(PyObject *self, PyObject *args, PyObject *kwargs)
+{
+    pyfr_mod_state_t *state = pyfr_get_mod_state();
+    const char * const keywords[] = { "raddb_dir", "dict_dir", "lib_dir", "debug_lvl", NULL};
+    char *raddb_dir = NULL, *dict_dir = NULL, *lib_dir = NULL;
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|sssi", UNCONST(char **, keywords), &raddb_dir, &dict_dir, &lib_dir, &fr_debug_lvl)) return NULL;
+    DEBUG2("raddb_dir='%s', dict_dir='%s', lib_dir='%s'", raddb_dir, dict_dir, lib_dir);
+
+#define _SET_VAR(var, dflt)  state->var = talloc_strdup(NULL, (var && strlen(var)) > 0 ? var : dflt)
+
+    _SET_VAR(raddb_dir, RADDBDIR);
+    _SET_VAR(dict_dir, DICTDIR);
+    _SET_VAR(lib_dir, LIBDIR);
+
+    /*
+     *  It's easier having two sets of flags to set the
+     *  verbosity of library calls and the verbosity of
+     *  library.
+     */
+    fr_debug_lvl = 0;
+    fr_log_fp = stdout; // TODO: Move to some API settings.
+
+    return (PyObject *)self;
+}
+
+PYFR_INTERNAL PyObject *pyfr_set_raddb_dir(PyObject *self, PyObject *args)
+{
+    char *raddb_dir = NULL;
+
+    if (PyArg_ParseTuple(args, "s", &raddb_dir)) {
+        pyfr_mod_state_t *state = pyfr_get_mod_state();
+
+        DEBUG3("raddb_dir='%s'", raddb_dir);
+
+        state->raddb_dir = talloc_strdup(NULL, (raddb_dir && strlen(raddb_dir) > 0) ? raddb_dir : RADDBDIR);
+    }
+
+    return (PyObject *)self;
+}
+
+PYFR_INTERNAL PyObject *pyfr_set_dict_dir(PyObject *self, PyObject *args)
+{
+    char *dict_dir = NULL;
+
+    if (PyArg_ParseTuple(args, "s", &dict_dir)) {
+        pyfr_mod_state_t *state = pyfr_get_mod_state();
+
+        DEBUG3("dict_dir='%s'", dict_dir);
+
+        state->dict_dir = talloc_strdup(NULL, (dict_dir && strlen(dict_dir) > 0) ? dict_dir : DICTDIR);
+    }
+
+    return (PyObject *)self;
+}
+
+PYFR_INTERNAL PyObject *pyfr_set_lib_dir(PyObject *self, PyObject *args)
+{
+    char *lib_dir = NULL;
+
+    if (PyArg_ParseTuple(args, "s", &lib_dir)) {
+        pyfr_mod_state_t *state = pyfr_get_mod_state();
+
+        DEBUG3("lib_dir='%s'", lib_dir);
+
+        state->lib_dir = talloc_strdup(NULL, (lib_dir && strlen(lib_dir) > 0) ? lib_dir : LIBDIR);
+    }
+
+    return (PyObject *)self;
+}
+
+PYFR_INTERNAL PyObject *pyfr_set_debug_level(PyObject *self, PyObject *args)
+{
+    if (PyArg_ParseTuple(args, "i", &fr_debug_lvl)) DEBUG3("fr_debug_lvl='%d'", fr_debug_lvl);
+
+    return (PyObject *)self;
+}
+
+PYFR_INTERNAL PyObject *pyfr_version_info(UNUSED PyObject *self, UNUSED PyObject *args)
+{
+    PyObject *ret = NULL;
+    PyObject *tmp;
+
+    ret = PyTuple_New((Py_ssize_t)4); /* (pyfr_version, git_hash, arch, built) */
+    if (ret == NULL) goto error;
+
+#define SET(i, v) \
+        tmp = (v); if (tmp == NULL) goto error; PyTuple_SET_ITEM(ret, i, tmp)
+    SET(0, PyUnicode_FromString(pyfr_version));
+    SET(1, PyUnicode_FromString(PYFR_VERSION_COMMIT_STRING));
+    SET(2, PyUnicode_FromString(HOSTINFO));
+    SET(3, PyUnicode_FromString(_PYFR_VERSION_BUILD_TIMESTAMP));
+#undef SET
+
+    return ret;
+
+error:
+    Py_XDECREF(ret);
+    return NULL;
+}
+
+PYFR_INTERNAL void pyfr_mod_free(UNUSED void *unused) {
+
+#ifndef NDEBUG
+    talloc_free(pyfr_get_mod_state()->autofree);
+#endif
+
+    /*
+     *  Ensure our atexit handlers run before any other
+     *  atexit handlers registered by third party libraries.
+     */
+    fr_atexit_global_trigger_all();
+}
+
+/* List of functions defined in this module */
+PYFR_INTERNAL PyMethodDef pyfr_methods[] = {
+    { "PyFR",             (PyCFunction)pyfr_PyFR,            METH_VARARGS | METH_KEYWORDS, NULL },
+    { "set_raddb_dir",    (PyCFunction)pyfr_set_raddb_dir,   METH_VARARGS, NULL },
+    { "set_dict_dir",     (PyCFunction)pyfr_set_dict_dir,    METH_VARARGS, NULL },
+    { "set_lib_dir",      (PyCFunction)pyfr_set_lib_dir,     METH_VARARGS, NULL },
+    { "set_debug_level",  (PyCFunction)pyfr_set_debug_level, METH_VARARGS, NULL },
+    { "get_version_info", (PyCFunction)pyfr_version_info,    METH_NOARGS,  NULL },
+    { NULL, NULL, 0, NULL }
+};
+
+PYFR_INTERNAL PyModuleDef pyfr_module = {
+    PyModuleDef_HEAD_INIT,
+    .m_name = "pyfr",
+    .m_doc = "Python bindings for miscellaneous FreeRADIUS functions.",
+    .m_size = -1,
+    .m_methods = pyfr_methods,
+    .m_traverse = NULL,
+    .m_clear = NULL,
+    .m_free = pyfr_mod_free
+};
+
+PyMODINIT_FUNC PyInit_pyfr(void);
+PyMODINIT_FUNC PyInit_pyfr(void)
+{
+    PyObject *m;
+
+    m = PyModule_Create(&pyfr_module);
+    if (!m) return NULL;
+    
+    /* Add error object to the module */
+    pyfr_ErrorObject = PyErr_NewException("pyfr.ErrorObject", PyExc_RuntimeError, NULL);
+    if (pyfr_ErrorObject) {
+        Py_INCREF(pyfr_ErrorObject);
+        PyModule_AddObject(m, "ErrorObject", pyfr_ErrorObject);
+    }
+
+    /* Load some consts like version and default paths */
+    if (!pyfr_register_consts(m)) goto error;
+
+    /* then, let's call everything needed by libfreeradius* */
+    if (!pyfr_bootstrap_libfreeradius(m)) goto error;
+
+    /* Bootstrap all modules */
+    if (!pyfr_register_modules(m)) goto error;
+
+    return m;
+
+error:
+    if (!PyErr_Occurred()) PyErr_SetString(PyExc_ImportError, "pyfr module load failed");
+
+    Py_XDECREF(pyfr_ErrorObject);
+    Py_CLEAR(pyfr_ErrorObject);
+    Py_DECREF(m);
+
+    return NULL;
+}
diff --git a/src/language/python/src/pyfr.h b/src/language/python/src/pyfr.h
new file mode 100644 (file)
index 0000000..75a0582
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/pyfr.h
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+RCSIDH(pyfr_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <freeradius-devel/autoconf.h>
+#include <freeradius-devel/util/conf.h>
+#include <freeradius-devel/util/syserror.h>
+#include <freeradius-devel/util/atexit.h>
+#include <freeradius-devel/util/dict.h>
+#include <freeradius-devel/util/dict_priv.h>
+#include <freeradius-devel/util/version.h>
+
+#define PYFR_TYPE_FLAGS Py_TPFLAGS_HAVE_GC
+#define PYFR_SINGLE_FILE
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <pythread.h>
+#include <structmember.h>
+
+#if !(PY_MAJOR_VERSION >= 3 && PY_MINOR_VERSION >= 10) /* At least 3.10.x */ 
+    #error "We expect Python >= 3.10.x"
+#endif
+
+#if defined(PYFR_SINGLE_FILE)
+# define PYFR_INTERNAL static
+#else
+# define PYFR_INTERNAL
+#endif
+
+typedef struct {
+    PyObject_HEAD
+    PyObject *error;
+    bool util_loaded;
+    bool radius_loaded;
+
+    TALLOC_CTX *autofree;
+
+    char *raddb_dir; //!< Path to raddb directory
+    char *dict_dir;  //!< The location for loading dictionaries
+    char *lib_dir;   //!< The location for loading libraries
+} pyfr_mod_state_t;
+
+pyfr_mod_state_t *pyfr_get_mod_state(void);
+
+DIAG_OFF(unused-macros)
+#define DEBUG(fmt, ...)     if (fr_log_fp && (fr_debug_lvl > 1)) fr_fprintf(fr_log_fp , "** DEBUG: pyfr: %s:%d %s(): "fmt "\n", __FILE__, __LINE__, __func__, ## __VA_ARGS__)
+#define DEBUG2(fmt, ...)    if (fr_log_fp && (fr_debug_lvl > 2)) fr_fprintf(fr_log_fp , "** DEBUG2: pyfr: %s:%d %s(): "fmt "\n", __FILE__, __LINE__, __func__, ## __VA_ARGS__)
+#define DEBUG3(fmt, ...)    if (fr_log_fp && (fr_debug_lvl > 3)) fr_fprintf(fr_log_fp , "** DEBUG3: pyfr: %s:%d %s(): "fmt "\n", __FILE__, __LINE__, __func__, ## __VA_ARGS__)
+#define INFO(fmt, ...)      if (fr_log_fp && (fr_debug_lvl > 0)) fr_fprintf(fr_log_fp , "** INFO: pyfr: %s:%d %s(): "fmt "\n", __FILE__, __LINE__, __func__, ## __VA_ARGS__)
+DIAG_ON(unused-macros)
+
+#ifndef NDEBUG
+#   define pyfr_ErrorObject_as_strerror(pyErrorObj) PyErr_Format(pyErrorObj, "%s:%d %s(): %s", __FILE__, __LINE__, __func__, fr_strerror())
+#else
+#   define pyfr_ErrorObject_as_strerror(pyErrorObj) PyErr_SetString(pyErrorObj, fr_strerror())
+#endif
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/language/python/src/radius.c b/src/language/python/src/radius.c
new file mode 100644 (file)
index 0000000..5bc13d3
--- /dev/null
@@ -0,0 +1,537 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/radius.c
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+
+RCSID("$Id$")
+
+#include "src/pyfr.h"
+#include "src/radius.h"
+
+#include <freeradius-devel/util/pair_legacy.h>
+#include <freeradius-devel/util/proto.h>
+#include <freeradius-devel/radius/radius.h>
+#include <freeradius-devel/protocol/radius/freeradius.internal.h>
+
+extern fr_dict_t const *dict_freeradius;
+extern fr_dict_t const *dict_radius;
+
+PyObject *pyfr_ErrorRadius = NULL;
+
+PYFR_INTERNAL int pyfr_radius_init(UNUSED PyObject *self, UNUSED PyObject *args, UNUSED PyObject *kwds)
+{
+    pyfr_mod_state_t  *state = pyfr_get_mod_state();
+
+    if (state->radius_loaded) return 0;
+
+    DEBUG3("Initialising libfreeradius-radius");
+
+    if (fr_radius_init() < 0) {
+        PyErr_Format(pyfr_ErrorRadius, "fr_radius_init() Failed: %s", fr_strerror());
+        goto error;
+    }
+
+    state->radius_loaded = true;
+
+    return 0;
+
+error:
+    return -1;
+}
+
+PYFR_INTERNAL PyObject *pyfr_radius_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    const char * const keywords[] = { "auth_host", "auth_port", NULL};
+    char const *auth_host = NULL, *auth_port = NULL;
+    pyfr_radius_ctx_t *ctx;
+    
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ss", UNCONST(char **, keywords), &auth_host, &auth_port)) return NULL;
+
+    if (!auth_host) auth_host = "127.0.0.1";
+    if (!auth_port) auth_port = "1812";
+
+    ctx = PyObject_New(pyfr_radius_ctx_t, type);
+    ctx->auth_host = talloc_strdup(NULL, auth_host);
+    ctx->auth_port = talloc_strdup(NULL, auth_port);
+
+    return (PyObject *)ctx;
+}
+
+PYFR_INTERNAL void pyfr_radius_dealloc(PyObject *self)
+{
+    pyfr_radius_ctx_t *ctx = (pyfr_radius_ctx_t *)self;
+
+    fr_radius_free();
+
+    TALLOC_FREE(ctx->auth_host);
+    TALLOC_FREE(ctx->auth_port);
+
+    PyObject_Del(ctx);
+}
+
+static void *pyfr_radius_next_encodable(fr_dlist_head_t *list, void *current, void *uctx)
+{
+    fr_pair_t *vp = current;
+    fr_dict_t *dict = talloc_get_type_abort(uctx, fr_dict_t);
+
+    while ((vp = fr_dlist_next(list, vp))) {
+        PAIR_VERIFY(vp);
+        if ((vp->da->dict == dict) &&
+            (!vp->da->flags.internal || ((vp->da->attr > FR_TAG_BASE) && (vp->da->attr < (FR_TAG_BASE + 0x20))))) {
+            break;
+        }
+    }
+
+    return vp;
+}
+
+PYFR_INTERNAL PyObject *pyfr_radius_encode_pair(UNUSED PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    const char * const keywords[] = { "attrs", "secret", NULL};
+    PyObject         *data = NULL, *kattrs, *key, *value_list;
+    Py_ssize_t       pos = 0;
+    fr_pair_t        *vp;
+    fr_pair_list_t   tmp_list;
+    fr_dict_t const  *dict = dict_radius;
+    fr_dcursor_t     cursor;
+    fr_dbuff_t       work_dbuff;
+    char             buff[MAX_PACKET_LEN];
+    char             *ksecret, *secret = NULL;
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Os", UNCONST(char **, keywords), &kattrs, &ksecret)) {
+        PyErr_SetString(pyfr_ErrorRadius, "Invalid Argument. e.g: attrs={ \"attribute1\": [ \"arg1\" ], \"attribute2\": [ \"arg2\" , ... ] }");
+        return NULL;
+    }
+
+    fr_pair_list_init(&tmp_list);
+
+    /*
+     * Walk through the Radius.encode_pair(..., attrs={ "attrs": [ "arg" ], ... }, ...)
+     * parameters and build VPs list
+     */
+    while (PyDict_Next(kattrs, &pos, &key, &value_list)) {
+        if (!PyList_Check(value_list)) {
+            PyErr_Format(pyfr_ErrorRadius, "Wrong argument at position %ld, it must be a 'list'. e.g: attrs={\"attribute\": [ \"arg1\", ... ] }", pos);
+            goto error;
+        }
+
+        for (int i = 0; i < PyList_Size(value_list); i++) {
+            char const *lhs, *rhs;
+            char *lhs_rhs;
+
+            lhs = PyUnicode_AsUTF8(key);
+            rhs = PyUnicode_AsUTF8(PyList_GetItem(value_list, i));
+            lhs_rhs = talloc_asprintf(NULL, "%s=\"%s\"", lhs, rhs);
+
+            DEBUG2("Encode %s", lhs_rhs);
+
+            if (fr_pair_list_afrom_str(NULL, fr_dict_root(dict_radius), lhs_rhs, strlen(lhs_rhs), &tmp_list) != T_EOL) {
+                pyfr_ErrorObject_as_strerror(pyfr_ErrorRadius);
+                talloc_free(lhs_rhs);
+                goto error;
+            }
+
+            talloc_free(lhs_rhs);
+        }
+    }
+
+    /*
+     *  Output may be an error, and we return it if so.
+     */
+    if (fr_pair_list_empty(&tmp_list)) {
+        PyErr_SetString(pyfr_ErrorRadius, "Empty avp list.");
+        goto error;
+    }
+
+    fr_dbuff_init(&work_dbuff, buff, sizeof(buff));
+
+    /* fr_radius_encode_pair() expects talloced 'secret' parameter */
+    secret = talloc_strdup(NULL, ksecret);
+
+    /*
+     *  Loop over the reply attributes for the packet.
+     */
+    fr_pair_dcursor_iter_init(&cursor, &tmp_list, pyfr_radius_next_encodable, dict);
+    while ((vp = fr_dcursor_current(&cursor))) {
+        PAIR_VERIFY(vp);
+
+        DEBUG3("Calling fr_radius_encode_pair() for %pP (%s).", vp, fr_type_to_str(vp->da->type));
+
+        /*
+         *  Encode an individual VP
+         */
+        if (fr_radius_encode_pair(&work_dbuff, &cursor, &(fr_radius_ctx_t){ .secret = secret }) < 0) {
+            pyfr_ErrorObject_as_strerror(pyfr_ErrorRadius);
+            goto error;
+        }
+    }
+
+    FR_PROTO_HEX_DUMP(fr_dbuff_start(&work_dbuff), fr_dbuff_used(&work_dbuff), "%s encoded packet", __FUNCTION__);
+
+    data = Py_BuildValue("y#", fr_dbuff_start(&work_dbuff),fr_dbuff_used(&work_dbuff));
+    if (!data) {
+        PyErr_SetString(pyfr_ErrorRadius, "Py_BuildValue() failed.");
+        goto error;
+    }
+
+error:
+    TALLOC_FREE(secret);
+
+    /* clean up and return result */
+    fr_pair_list_free(&tmp_list);
+
+    return data;
+}
+
+PYFR_INTERNAL PyObject *pyfr_radius_decode_pair(UNUSED PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    PyObject           *attrs = NULL;
+    fr_pair_t          *vp;
+    fr_pair_list_t     tmp_list;
+    fr_dcursor_t       cursor;
+    const char * const keywords[] = { "data", "secret", NULL};
+    char               *ksecret;
+    uint8_t            *kdata, *ptr;
+    size_t             kdata_len, ptr_len, my_len;
+    pyfr_mod_state_t   *state = pyfr_get_mod_state();
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y#s", UNCONST(char **, keywords), &kdata, &kdata_len, &ksecret)) {
+        PyErr_SetString(pyfr_ErrorRadius, "Invalid parameter, expecting packet payload.");
+        return NULL;
+    }
+
+    FR_PROTO_HEX_DUMP(kdata, kdata_len, "%s decode packet", __FUNCTION__);
+
+    ptr     = kdata;
+    ptr_len = kdata_len;
+
+    fr_pair_list_init(&tmp_list);
+
+    /*
+     *  Loop over the attributes, decoding them into VPs.
+     */
+    while (ptr_len > 0) {
+        my_len = fr_radius_decode_pair(state->autofree, &tmp_list, ptr, ptr_len, &(fr_radius_ctx_t){ .secret = ksecret, .end = (kdata + kdata_len) });
+        if (my_len < 0) {
+            PyErr_Format(pyfr_ErrorRadius, "fr_radius_decode_pair() returned %ld. (%s)", my_len, fr_strerror());
+            goto error;
+        }
+
+        /*
+        *  If my_len is larger than the room in the packet,
+        *  all kinds of bad things happen.
+        */
+        if (!fr_cond_assert(my_len <= ptr_len)) goto error;
+
+        ptr += my_len;
+        ptr_len -= my_len;
+    }
+
+    if (fr_pair_list_num_elements(&tmp_list) < 1) {
+        PyErr_SetString(pyfr_ErrorRadius, "Failed decoding packet");
+        goto error;
+    }
+
+    attrs = PyDict_New();
+    for (vp = fr_pair_dcursor_init(&cursor, &tmp_list);
+         vp;
+         vp = fr_dcursor_next(&cursor)) {
+        PyObject *value_list;
+        char lhs[64], rhs[128];
+
+        PAIR_VERIFY(vp);
+
+        DEBUG3("Decoding %pP", vp);
+
+        fr_dict_attr_oid_print(&FR_SBUFF_OUT(lhs, sizeof(lhs)), NULL, vp->da, false);
+        fr_pair_print_value_quoted(&FR_SBUFF_OUT(rhs, sizeof(rhs)), vp, T_BARE_WORD);
+
+        /* the RHS already exists? then, append it */
+        value_list = PyDict_GetItemString(attrs, lhs);
+        if (!value_list) value_list = PyList_New(0);
+
+        PyList_Append(value_list, PyUnicode_FromString(rhs));
+        PyDict_SetItemString(attrs, lhs, value_list);
+    }
+
+error:
+    /* clean up and return result */
+    fr_pair_list_free(&tmp_list);
+
+    return attrs;
+}
+
+PYFR_INTERNAL PyObject *pyfr_radius_encode_packet(UNUSED PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    const char * const keywords[] = { "attrs", "id", "secret", NULL};
+    uint8_t            kpacket_id = 0;
+    char               *ksecret = NULL, *secret = NULL;
+    Py_ssize_t         pos = 0, i =0;
+    PyObject           *data = NULL, *kattrs, *key, *value_list;
+    fr_pair_t          *vp;
+    fr_pair_list_t     tmp_list;
+    uint8_t            buff[MAX_PACKET_LEN];
+    ssize_t            slen;
+    char               *lhs_rhs;
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OBs", UNCONST(char **, keywords), &kattrs, &kpacket_id, &ksecret)) return NULL;
+
+    fr_pair_list_init(&tmp_list);
+
+    /*
+     * Walk through the Radius.encode_packet(..., attrs={ "attrs": [ "arg" ], ... }, ...)
+     * parameters and build VPs list
+     */
+    while (PyDict_Next(kattrs, &pos, &key, &value_list)) {
+        if (!PyList_Check(value_list)) {
+            PyErr_SetString(pyfr_ErrorRadius, "Wrong argument, it must be a 'list'. e.g: \"attribute\": [ \"arg1\", ... ]");
+            goto error;
+        }
+
+        for (i = 0; i < PyList_Size(value_list); i++) {
+            char const *lhs, *rhs;
+            
+            lhs = PyUnicode_AsUTF8(key);
+            rhs = PyUnicode_AsUTF8(PyList_GetItem(value_list, i));
+            lhs_rhs = talloc_asprintf(NULL, "%s=\"%s\"", lhs, rhs);
+
+            DEBUG2("Encode %s", lhs_rhs);
+
+            if (fr_pair_list_afrom_str(NULL, fr_dict_root(dict_radius), lhs_rhs, strlen(lhs_rhs), &tmp_list) != T_EOL) {
+                pyfr_ErrorObject_as_strerror(pyfr_ErrorRadius);
+                talloc_free(lhs_rhs);
+                goto error;
+            }
+
+            talloc_free(lhs_rhs);
+        }
+    }
+
+    /*
+     *  Output may be an error, and we return it if so.
+     */
+    if (fr_pair_list_empty(&tmp_list)) {
+        PyErr_SetString(pyfr_ErrorRadius, "Empty avp list");
+        goto error;
+    }
+
+    /* We can't go without Packet-Type */
+    vp = fr_pair_find_by_child_num(&tmp_list, NULL, fr_dict_root(dict_radius), FR_PACKET_TYPE);
+    if (!vp) {
+        PyErr_SetString(pyfr_ErrorRadius, "We can not go without 'Packet-Type' attribute.");
+        goto error;
+    }
+
+    /* fr_radius_encode_pair() expects talloced 'secret' parameter */
+    secret = talloc_strdup(NULL, ksecret);
+
+    slen = fr_radius_encode(buff, sizeof(buff), NULL, secret, strlen(secret), vp->vp_uint32, kpacket_id, &tmp_list);
+    if (slen < 0) {
+        pyfr_ErrorObject_as_strerror(pyfr_ErrorRadius);
+        goto error;
+    }
+
+    FR_PROTO_HEX_DUMP(buff, slen, "%s encoded data", __FUNCTION__);
+
+    data = Py_BuildValue("y#", buff, slen);
+    if (!data) {
+        PyErr_SetString(pyfr_ErrorRadius, "Py_BuildValue() failed.");
+        goto error;
+    }
+
+error:
+    /* clean up and return result */
+
+    TALLOC_FREE(secret);
+
+    fr_pair_list_free(&tmp_list);
+    return data;
+}
+
+PYFR_INTERNAL PyObject *pyfr_radius_decode_packet(UNUSED PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    PyObject           *ret = NULL, *attrs;
+    fr_pair_t          *vp;
+    fr_pair_list_t     tmp_list;
+    fr_dcursor_t       cursor;
+    const char * const keywords[] = { "data", "secret", NULL};
+    char               *ksecret, *secret;
+    uint8_t const      *kdata;
+    size_t             kdata_len;
+    uint8_t            packet_id = 0;
+    pyfr_mod_state_t   *state = pyfr_get_mod_state();
+
+    /* get one argument as an iterator */
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y#s", UNCONST(char **, keywords), &kdata, &kdata_len, &ksecret)) return NULL;
+
+    FR_PROTO_HEX_DUMP(kdata, kdata_len, "%s decode packet", __FUNCTION__);
+
+    fr_pair_list_init(&tmp_list);
+
+    secret = talloc_strdup(NULL, ksecret); /* Internally the fr_radius_decode_tunnel_password() expects a talloc's secret string. */
+    if (fr_radius_decode(state->autofree, &tmp_list, kdata, kdata_len, NULL, secret, talloc_array_length(secret) - 1) < 0) {
+        PyErr_SetString(pyfr_ErrorRadius, "Failed decoding packet");
+        goto error;
+    }
+
+    if (fr_pair_list_num_elements(&tmp_list) < 1) {
+        PyErr_SetString(pyfr_ErrorRadius, "Failed decoding packet");
+        goto error;
+    }
+
+    /* Add the virtual Packet-Type attribute */
+    vp = fr_pair_afrom_child_num(NULL, fr_dict_root(dict_radius), FR_PACKET_TYPE);
+    if (!vp) {
+        PyErr_SetString(pyfr_ErrorRadius, "Failed fr_pair_afrom_child_num(..., ..., FR_PACKET_TYPE)");
+        goto error;
+    }
+    vp->vp_uint32 = kdata[0];
+    fr_pair_prepend(&tmp_list, vp);
+
+    /* Set packet id */
+    packet_id = kdata[1];
+
+    /* let's walkthrough the packets */
+    attrs = PyDict_New();
+    for (vp = fr_pair_dcursor_init(&cursor, &tmp_list);
+         vp;
+         vp = fr_dcursor_next(&cursor)) {
+        PyObject *value_list;
+        char lhs[64], rhs[128];
+
+        PAIR_VERIFY(vp);
+
+        DEBUG2("Decoding %pP", vp);
+
+        fr_dict_attr_oid_print(&FR_SBUFF_OUT(lhs, sizeof(lhs)), NULL, vp->da, false);
+        fr_pair_print_value_quoted(&FR_SBUFF_OUT(rhs, sizeof(rhs)), vp, T_BARE_WORD);
+
+        /* the RHS already exists? then, append it */
+        value_list = PyDict_GetItemString(attrs, lhs);
+        if (!value_list) value_list = PyList_New(0);
+
+        PyList_Append(value_list, PyUnicode_FromString(rhs));
+        PyDict_SetItemString(attrs, lhs, value_list);
+    }
+
+    /* then, built the return */
+    ret = Py_BuildValue("i,O", packet_id, attrs);
+    if (!ret) {
+        PyErr_SetString(pyfr_ErrorRadius, "Py_BuildValue() failed.");
+        goto error;
+    }
+
+error:
+    TALLOC_FREE(secret);
+
+    /* clean up and return result */
+    fr_pair_list_free(&tmp_list);
+
+    return ret;
+}
+
+PYFR_INTERNAL PyMemberDef pyfr_radius_members[] = {
+    {"host", T_STRING, offsetof(pyfr_radius_ctx_t, auth_host), 0, "RADIUS host"},
+    {"port", T_STRING, offsetof(pyfr_radius_ctx_t, auth_port), 0, "RADIUS port"},
+    { NULL }  /* Sentinel */
+};
+
+/* List of functions defined in this module */
+PYFR_INTERNAL PyMethodDef pyfr_radius_methods[] = {
+    {
+        "encode_pair", (PyCFunction)pyfr_radius_encode_pair, (METH_VARARGS | METH_KEYWORDS),
+        "Encode a data structure into a RADIUS attribute."
+        "This is the main entry point into the encoder.  It sets up the encoder array"
+        "we use for tracking our TLV/VSA nesting and then calls the appropriate"
+        "dispatch function."
+    },
+
+    {
+        "decode_pair", (PyCFunction)pyfr_radius_decode_pair, (METH_VARARGS | METH_KEYWORDS),
+        "Decode a raw RADIUS packet into VPs."
+    },
+
+    {
+        "encode_packet", (PyCFunction)pyfr_radius_encode_packet, (METH_VARARGS | METH_KEYWORDS),
+        "Encode a data structure into a RADIUS attribute and reply as a dict() table."
+        "This is the main entry point into the encoder.  It sets up the encoder array"
+        "we use for tracking our TLV/VSA nesting and then calls the appropriate"
+        "dispatch function."
+    },
+
+    {
+        "decode_packet", (PyCFunction)pyfr_radius_decode_packet, (METH_VARARGS | METH_KEYWORDS),
+        "Decode a raw RADIUS packet into VPs."
+        "It returns: packet_id, attrs"
+    },
+
+    { NULL }
+};
+
+PYFR_INTERNAL PyTypeObject pyfr_radius_ctx_types = {
+    PyVarObject_HEAD_INIT(NULL, 0) "pyfr.Radius",                /* tp_name */
+    sizeof(pyfr_radius_ctx_t),                                   /* tp_basicsize */
+    0,                                                           /* tp_itemsize */
+    pyfr_radius_dealloc,                                         /* tp_dealloc */
+    0,                                                           /* tp_print */
+    0,                                                           /* tp_getattr */
+    0,                                                           /* tp_setattr */
+    0,                                                           /* tp_reserved */
+    0,                                                           /* tp_repr */
+    0,                                                           /* tp_as_number */
+    0,                                                           /* tp_as_sequence */
+    0,                                                           /* tp_as_mapping */
+    0,                                                           /* tp_hash  */
+    0,                                                           /* tp_call */
+    0,                                                           /* tp_str */
+    0,                                                           /* tp_getattro */
+    0,                                                           /* tp_setattro */
+    0,                                                           /* tp_as_buffer */
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,                    /* tp_flags*/
+    "Object to use libfreeradius-radius library.",               /* tp_doc */
+    0,                                                           /* tp_traverse */
+    0,                                                           /* tp_clear */
+    0,                                                           /* tp_richcompare */
+    0,                                                           /* tp_weaklistoffset */
+    0,                                                           /* tp_iter */
+    0,                                                           /* tp_iternext */
+    pyfr_radius_methods,                                         /* tp_methods */
+    pyfr_radius_members,                                         /* tp_members */
+    0,                                                           /* tp_getset */
+    0,                                                           /* tp_base */
+    0,                                                           /* tp_dict */
+    0,                                                           /* tp_descr_get */
+    0,                                                           /* tp_descr_set */
+    0,                                                           /* tp_dictoffset */
+    pyfr_radius_init,                                            /* tp_init */
+    0,                                                           /* tp_alloc */
+    pyfr_radius_new,                                             /* tp_new */
+};
+
+PyTypeObject *pyfr_radius_register(void)
+{
+    DEBUG2("Loading pyfr.Radius");
+
+    return &pyfr_radius_ctx_types;
+}
diff --git a/src/language/python/src/radius.h b/src/language/python/src/radius.h
new file mode 100644 (file)
index 0000000..074bafc
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/radius.h
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+
+RCSIDH(pyfr_radius_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <pythread.h>
+
+extern PyObject *pyfr_ErrorRadius;
+
+typedef struct {
+    PyObject_HEAD
+    char *auth_host;  //!< auth host
+    char *auth_port;  //!< auth host port
+} pyfr_radius_ctx_t;
+
+PyTypeObject *pyfr_radius_register(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/language/python/src/util.c b/src/language/python/src/util.c
new file mode 100644 (file)
index 0000000..3a526b4
--- /dev/null
@@ -0,0 +1,231 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/util.c
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+
+RCSID("$Id$")
+
+#include "src/pyfr.h"
+#include "src/util.h"
+
+#include <freeradius-devel/util/pair_legacy.h>
+#include <freeradius-devel/util/proto.h>
+#include <freeradius-devel/radius/radius.h>
+#include <freeradius-devel/protocol/radius/freeradius.internal.h>
+
+PyObject *pyfr_ErrorUtil = NULL;
+
+fr_dict_t const *dict_freeradius;
+fr_dict_t const *dict_radius;
+
+extern fr_dict_autoload_t pyfr_dict[];
+fr_dict_autoload_t pyfr_dict[] = {
+    { .out = &dict_freeradius, .proto = "freeradius" },
+    { .out = &dict_radius, .proto = "radius" },
+    { NULL }
+};
+
+PYFR_INTERNAL int pyfr_util_init(UNUSED PyObject *self, UNUSED PyObject *args, UNUSED PyObject *kwds)
+{
+    pyfr_mod_state_t *state = pyfr_get_mod_state();
+
+    if (state->util_loaded) return 0;
+
+    DEBUG3("Initialising libfreeradius-util");
+
+    /*
+     *  Initialize the DL infrastructure, which is used by the
+     *  config file parser.
+     */
+    if (state->lib_dir && dl_search_global_path_set(state->lib_dir) < 0) {
+        pyfr_ErrorObject_as_strerror(pyfr_ErrorUtil);
+        goto error;
+    }
+
+    /* Load the dictionary */
+    if (!fr_dict_global_ctx_init(NULL, true, state->dict_dir)) {
+        pyfr_ErrorObject_as_strerror(pyfr_ErrorUtil);
+        goto error;
+    }
+
+    if (fr_dict_autoload(pyfr_dict) < 0) {
+        pyfr_ErrorObject_as_strerror(pyfr_ErrorUtil);
+        goto error;
+    }
+
+    if (fr_dict_read(fr_dict_unconst(dict_freeradius), state->raddb_dir, FR_DICTIONARY_FILE) == -1) {
+        fr_log_perror(&default_log, L_ERR, __FILE__, __LINE__, NULL, "fr_dict_read() Failed to initialize the dictionaries");
+        PyErr_Format(pyfr_ErrorUtil, "fr_dict_read() Failed initialising the dictionaries");
+        goto error;
+    }
+
+    if (fr_dict_read(fr_dict_unconst(dict_radius), state->raddb_dir, FR_DICTIONARY_FILE) == -1) {
+        fr_log_perror(&default_log, L_ERR, __FILE__, __LINE__, NULL, "fr_dict_read() Failed to initialize the dictionaries");
+        PyErr_Format(pyfr_ErrorUtil, "fr_dict_read() Failed initialising the dictionaries");
+        goto error;
+    }
+
+    state->util_loaded = true;
+
+    return 1;
+
+error:
+    return -1;
+}
+
+PYFR_INTERNAL PyObject *pyfr_util_new(PyTypeObject *type, UNUSED PyObject *args, UNUSED PyObject *kwargs)
+{
+    pyfr_util_ctx_t *ctx = PyObject_New(pyfr_util_ctx_t, type);
+    pyfr_mod_state_t *state = pyfr_get_mod_state();
+
+    DEBUG2("raddb_dir='%s', dict_dir='%s', lib_dir='%s'", state->raddb_dir, state->dict_dir, state->lib_dir);
+
+    return (PyObject *)ctx;
+}
+
+PYFR_INTERNAL void pyfr_util_dealloc(PyObject *self)
+{
+    pyfr_util_ctx_t *ctx = (pyfr_util_ctx_t *)self;
+
+    if (fr_dict_autofree(pyfr_dict) < 0) pyfr_ErrorObject_as_strerror(pyfr_ErrorUtil);
+
+    PyObject_Del(ctx);
+}
+
+PYFR_INTERNAL PyObject *pyfr_util_dict_attr_by_oid(UNUSED PyObject *self, PyObject *args)
+{
+    PyObject             *obj;
+    const char           *oid;
+    fr_dict_attr_t const *da;
+    char                  flags_str[256];
+    char                  oid_str[512];
+    char                  oid_num[16];
+    pyfr_mod_state_t     *state = pyfr_get_mod_state();
+
+    if (!PyArg_ParseTuple(args, "s", &oid)) return NULL;
+
+    DEBUG3("Looking for \"%s\" in dict RADIUS", oid);
+
+    da = fr_dict_attr_by_oid(state->autofree, fr_dict_root(dict_radius), oid);
+    if (!da) {
+        PyErr_Format(pyfr_ErrorUtil, "OID '%s' not found", oid);
+        return NULL;
+    }
+
+    if (fr_dict_attr_oid_print(&FR_SBUFF_OUT(oid_str, sizeof(oid_str)), NULL, da, false) <= 0) {
+        PyErr_SetString(pyfr_ErrorUtil, "OID string too long");
+        return NULL;
+    }
+
+    if (fr_dict_attr_oid_print(&FR_SBUFF_OUT(oid_num, sizeof(oid_num)), NULL, da, true) <= 0) {
+        PyErr_SetString(pyfr_ErrorUtil, "OID string too long");
+        return NULL;
+    }
+
+    fr_dict_attr_flags_print(&FR_SBUFF_OUT(flags_str, sizeof(flags_str)), dict_radius, da->type, &da->flags);
+
+    obj = Py_BuildValue("{s:s, s:s, s:s, s:i, s:s, s:s, s:N, s:N, s:N, s:N, s:N, s:N, s:s}",
+                         "oid.string", oid_str,
+                         "oid.numeric", oid_num,
+                         "name", da->name,
+                         "id", da->attr,
+                         "type", fr_type_to_str(da->type),
+                         "flags", flags_str,
+                         "is_root", PyBool_FromLong(da->flags.is_root),
+                         "is_raw", PyBool_FromLong(da->flags.is_raw),
+                         "is_alias", PyBool_FromLong(da->flags.is_alias),
+                         "is_internal", PyBool_FromLong(da->flags.internal),
+                         "has_value", PyBool_FromLong(da->flags.has_value),
+                         "virtual", PyBool_FromLong(da->flags.virtual),
+                         "parent.type", fr_type_to_str(da->parent->type)
+    );
+
+    if (!obj) {
+        PyErr_SetString(pyfr_ErrorUtil, "Problems in Py_BuildValue()");
+        return NULL;
+    }
+
+    return obj;
+}
+
+PYFR_INTERNAL PyMemberDef pyfr_util_members[] = {
+    { NULL }  /* Sentinel */
+};
+
+/* List of functions defined in this module */
+PYFR_INTERNAL PyMethodDef pyfr_util_methods[] = {
+    {
+        "dict_attr_by_oid", pyfr_util_dict_attr_by_oid, METH_VARARGS,
+        "Resolve an attribute using an OID string."
+    },
+
+    { NULL }
+};
+
+PYFR_INTERNAL PyTypeObject pyfr_util_types = {
+    PyVarObject_HEAD_INIT(NULL, 0) "pyfr.Util",                  /* tp_name */
+    sizeof(pyfr_util_ctx_t),                                     /* tp_basicsize */
+    0,                                                           /* tp_itemsize */
+    pyfr_util_dealloc,                                           /* tp_dealloc */
+    0,                                                           /* tp_print */
+    0,                                                           /* tp_getattr */
+    0,                                                           /* tp_setattr */
+    0,                                                           /* tp_reserved */
+    0,                                                           /* tp_repr */
+    0,                                                           /* tp_as_number */
+    0,                                                           /* tp_as_sequence */
+    0,                                                           /* tp_as_mapping */
+    0,                                                           /* tp_hash  */
+    0,                                                           /* tp_call */
+    0,                                                           /* tp_str */
+    0,                                                           /* tp_getattro */
+    0,                                                           /* tp_setattro */
+    0,                                                           /* tp_as_buffer */
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,                    /* tp_flags*/
+    "Object to use libfreeradius-util library.",                 /* tp_doc */
+    0,                                                           /* tp_traverse */
+    0,                                                           /* tp_clear */
+    0,                                                           /* tp_richcompare */
+    0,                                                           /* tp_weaklistoffset */
+    0,                                                           /* tp_iter */
+    0,                                                           /* tp_iternext */
+    pyfr_util_methods,                                           /* tp_methods */
+    pyfr_util_members,                                           /* tp_members */
+    0,                                                           /* tp_getset */
+    0,                                                           /* tp_base */
+    0,                                                           /* tp_dict */
+    0,                                                           /* tp_descr_get */
+    0,                                                           /* tp_descr_set */
+    0,                                                           /* tp_dictoffset */
+    pyfr_util_init,                                              /* tp_init */
+    0,                                                           /* tp_alloc */
+    pyfr_util_new,                                               /* tp_new */
+};
+
+PyTypeObject *pyfr_util_register(void)
+{
+    DEBUG2("Loading pyfr.Util");
+
+    return &pyfr_util_types;
+}
diff --git a/src/language/python/src/util.h b/src/language/python/src/util.h
new file mode 100644 (file)
index 0000000..6ac0664
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   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, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file src/util.h
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+
+RCSIDH(pyfr_util_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <pythread.h>
+
+extern PyObject *pyfr_ErrorUtil;
+
+typedef struct {
+    PyObject_HEAD
+} pyfr_util_ctx_t;
+
+PyTypeObject *pyfr_util_register(void);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/language/python/src/version.h b/src/language/python/src/version.h
new file mode 100644 (file)
index 0000000..2ba0935
--- /dev/null
@@ -0,0 +1,92 @@
+#pragma once
+/*
+ *   This library is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU Lesser General Public
+ *   License as published by the Free Software Foundation; either
+ *   version 2.1 of the License, or (at your option) any later version.
+ *
+ *   This library 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
+ *   Lesser General Public License for more details.
+ *
+ *   You should have received a copy of the GNU Lesser General Public
+ *   License along with this library; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/** Version checking functions
+ *
+ * @file src/version.h
+ * @brief Python bindings for major FreeRADIUS libraries
+ *
+ * @copyright Network RADIUS SAS(legal@networkradius.com)
+ * @author 2023 Jorge Pereira (jpereira@freeradius.org)
+ */
+RCSIDH(src_version_h, "$Id$")
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+
+#ifndef NDEBUG
+#  define PYFR_VERSION_DEVELOPER "DEVELOPER BUILD - "
+#else
+#  define PYFR_VERSION_DEVELOPER ""
+#endif
+
+#if !defined(PYFR_VERSION_COMMIT) && defined(RADIUSD_VERSION_COMMIT)
+#define PYFR_VERSION_COMMIT RADIUSD_VERSION_COMMIT
+#endif
+
+#ifdef PYFR_VERSION_COMMIT
+#  define PYFR_VERSION_COMMIT_STRING " (git #" STRINGIFY(PYFR_VERSION_COMMIT) ")"
+#else
+#  define PYFR_VERSION_COMMIT_STRING ""
+#endif
+
+#ifndef ENABLE_REPRODUCIBLE_BUILDS
+#  define _PYFR_VERSION_BUILD_TIMESTAMP "built on " __DATE__ " at " __TIME__
+#  define PYFR_VERSION_BUILD_TIMESTAMP ", "_PYFR_VERSION_BUILD_TIMESTAMP
+#else
+#  define PYFR_VERSION_BUILD_TIMESTAMP ""
+#endif
+
+/** Create a version string for a utility in the suite of FreeRADIUS utilities
+ *
+ * @param _x utility name
+ */
+#define PYFR_VERSION_BUILD() \
+       PYFR_VERSION_DEVELOPER \
+       "version " \
+       STRINGIFY(PYFR_VERSION_MAJOR) "." STRINGIFY(PYFR_VERSION_MINOR) "." STRINGIFY(PYFR_VERSION_INCRM) \
+       PYFR_VERSION_COMMIT_STRING \
+       ", for host " HOSTINFO \
+       PYFR_VERSION_BUILD_TIMESTAMP
+
+#ifdef WITHOUT_VERSION_CHECK
+#  define PYFR_MAGIC_NUMBER    ((uint64_t) (0xf4ee4ad3f4ee4ad3))
+#  define MAGIC_PREFIX(_x)     ((uint8_t) 0x00)
+#  define MAGIC_VERSION(_x)    ((uint32_t) 0x00000000)
+#else
+/*
+ *     Mismatch between debug builds between
+ *     the modules and the server causes all
+ *     kinds of strange issues.
+ */
+#  ifndef NDEBUG
+#    define MAGIC_PREFIX_DEBUG 01
+#  else
+#    define MAGIC_PREFIX_DEBUG  00
+#  endif
+#  define PYFR_MAGIC_NUMBER ((uint64_t) HEXIFY2(MAGIC_PREFIX_DEBUG, PYFR_VERSION))
+#  define MAGIC_PREFIX(_x)     ((uint8_t) ((0xff00000000000000 & (_x)) >> 56))
+#  define MAGIC_VERSION(_x)    ((uint32_t)((0x00ffffff00000000 & (_x)) >> 32))
+#  define MAGIC_COMMIT(_x)     ((uint32_t)((0x00000000ffffffff & (_x))))
+#endif
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/language/python/tests/run.sh b/src/language/python/tests/run.sh
new file mode 100755 (executable)
index 0000000..0136825
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+for _f in tests/*.py; do
+       echo "CALL $_f"
+       $_f
+done
diff --git a/src/language/python/tests/test.py b/src/language/python/tests/test.py
new file mode 100755 (executable)
index 0000000..69f8f45
--- /dev/null
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+#
+# Test script for pyfr
+# Copyright 2023 The FreeRADIUS server project
+# Author: Jorge Pereira (jpereira@freeradius.org)
+#
+
+import argparse
+import binascii
+import os
+import sys
+import time
+import json
+import traceback
+
+from pprint import pprint
+
+try:
+       import pyfr
+except pyfr.error as e:
+       print("ERROR: import pyfr {}".format(e))
+
+def load_args():
+       """
+       Load all parameters from the command line.
+       """
+       parser = argparse.ArgumentParser(description = "test script",
+                                                                        formatter_class = argparse.RawDescriptionHelpFormatter)
+       parser.add_argument("-v",
+                       dest = "verbose",
+                       help = "Verbose mode. (e.g: -vvv)",
+                       action = 'count',
+                       required = False,
+                       default = 0
+       )
+
+       parser.add_argument("-d",
+                       dest = "raddb_dir",
+                       help = "Set configuration directory (defaults {})".format(pyfr.RADDBDIR),
+                       required = False,
+                       default = pyfr.RADDBDIR
+       )
+       parser.add_argument("-D",
+                       dest = "dict_dir",
+                       help = "Path for 'dictionary' directory. (default: {})".format(pyfr.DICTDIR),
+                       required = False,
+                       default = pyfr.DICTDIR
+       )
+       parser.add_argument("-l",
+                       dest = "lib_dir",
+                       help = "Path for 'libraries' directory. (default: {})".format(pyfr.LIBDIR),
+                       required = False,
+                       default = pyfr.LIBDIR
+       )
+       args = parser.parse_args()
+
+       return args
+
+print("test.py: ###########################################################")
+print("test.py: # Consts")
+print("test.py: ###########################################################")
+
+fr = pyfr.PyFR()
+# fr = pyfr.PyFR(raddb_dir=raddb_dir, dict_dir=dict_dir, lib_dir=lib_dir, debug_lvl=10)
+
+args = load_args()
+
+fr.set_debug_level(args.verbose+2)
+fr.set_lib_dir(args.lib_dir)
+fr.set_raddb_dir(args.raddb_dir)
+fr.set_dict_dir(args.dict_dir)
+
+# pprint(vars(fr))
+
+print("test.py: ###########################################################")
+print("test.py: # pyfr.Util")
+print("test.py: ###########################################################")
+
+try:
+       u = fr.Util()
+       r = fr.Radius()
+
+       print()
+       print("test.py: ###########################################################")
+       print("test.py: Util.dict_attr_by_oid()")
+       print("test.py: ###########################################################")
+       attr = "Vendor-Specific.Alcatel.Client-Primary-DNS"
+       ret = u.dict_attr_by_oid(attr)
+       print("test.py: pyfr.Util.dict_attr_by_oid('{}') = {}".format(attr, json.dumps(ret, indent=4, sort_keys=True)))
+
+       print()
+       print("test.py: ###########################################################")
+       print("test.py: Radius.encode_pair()")
+       print("test.py: ###########################################################")
+       attrs = {
+               "User-Name": [ "hare", "krishina" ],
+               "User-Password": [ "jorge" ],
+               "Vendor-Specific.WiMAX.DNS-Server": [ "::1" ],
+               "Vendor-Specific.Alcatel.Client-Primary-DNS": [ "8.8.8.8", "8.6.6.6" ]
+       }
+       data = r.encode_pair(attrs=attrs, secret="testing123")
+       print("input:  {}".format(attrs))
+       print("output: {}".format(binascii.hexlify(data)))
+
+       print()
+       print("test.py: ###########################################################")
+       print("test.py: Radius.decode_pair()")
+       print("test.py: ###########################################################")
+       data = b'010668617265010a6b72697368696e611a19000060b5341300000000000000000000000000000000011a0c00000be10506080808081a0c00000be1050608060606'
+       attrs = r.decode_pair(data=binascii.unhexlify(data), secret="testing123")
+       print("input:  {}".format(data))
+       print("output: {}".format(attrs))
+
+       print()
+       print("test.py: ###########################################################")
+       print("test.py: Radius.encode_packet()")
+       print("test.py: ###########################################################")
+       attrs = {
+               "Packet-Type": [ "Access-Request" ],
+               "User-Name": [ "jorge", "pereira" ],
+               "User-Password": [ "jorge" ],
+               "Vendor-Specific.WiMAX.DNS-Server": [ "::1" ],
+               "Vendor-Specific.Alcatel.Client-Primary-DNS": [ "8.8.8.8" ]
+       }
+       packet_id = 202
+       data = r.encode_packet(attrs=attrs, id=packet_id, secret="testing123")
+       print("attrs:     {}".format(attrs))
+       print("packet-id: {}".format(packet_id))
+       print("packet:    {}".format(binascii.hexlify(data)))
+
+       print()
+       print("test.py: ###########################################################")
+       print("test.py: Radius.decode_packet()")
+       print("test.py: ###########################################################")
+       data = b'01ca00490000000000000000000000000000000001076a6f7267650109706572656972611a19000060b5341300000000000000000000000000000000011a0c00000be1050608080808'
+       packet_id, attrs = r.decode_packet(data=binascii.unhexlify(data), secret="testing123")
+       print("attrs:     {}".format(attrs))
+       print("packet-id: {}".format(packet_id))
+       print("packet:    {}".format(data))
+
+except Exception as e:
+       print("test.py: Problems with: {}".format(e))
+       traceback.print_exc()
+
+
+# print("###########################################################")
+# print("# pyfr.Radius")
+# print("###########################################################")
+# try:
+#      arg = "bar"
+#      radius = fr.Radius(auth_host="localhost", auth_port="1812")
+#      ret = radius.foo("tapioca")
+#      print("pyfr.radius.foo('{}') = {}".format(arg, json.dumps(ret, indent=4, sort_keys=True)))
+# except Exception as e:
+#      print("Problems with pyfr.radius.foo(): {}".format(e))
+#      traceback.print_exc()
\ No newline at end of file
diff --git a/src/language/python/tests/version.py b/src/language/python/tests/version.py
new file mode 100755 (executable)
index 0000000..7abaccc
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+#
+# Test script for pyfr
+# Copyright 2023 The FreeRADIUS server project
+# Author: Jorge Pereira (jpereira@freeradius.org)
+#
+
+import pyfr
+
+print("pyfr.version:                     {}".format(pyfr.version))
+print("pyfr.version_build:               {}".format(pyfr.version_build))
+print("pyfr.libfreeradius_version:       {}".format(pyfr.libfreeradius_version))
+print("pyfr.libfreeradius_version_build: {}".format(pyfr.libfreeradius_version_build))
+print("pyfr.version_info():              {}".format(pyfr.get_version_info()))