From 6a11d63c841a3c3dca83f3ea0974775726ec66ed Mon Sep 17 00:00:00 2001 From: Arran Cudbard-Bell Date: Thu, 4 Feb 2021 10:57:40 +0000 Subject: [PATCH] Python v4 nesting --- raddb/mods-available/python | 4 +- raddb/sites-available/default | 859 +--------------------------- src/modules/rlm_python/example.py | 11 +- src/modules/rlm_python/rlm_python.c | 818 +++++++++++++++++++++----- 4 files changed, 701 insertions(+), 991 deletions(-) diff --git a/raddb/mods-available/python b/raddb/mods-available/python index fad7008fd1..dfee4caa07 100644 --- a/raddb/mods-available/python +++ b/raddb/mods-available/python @@ -66,7 +66,7 @@ python { # The search path for Python modules. It must include the path to your # Python module. # -# python_path = ${modconfdir}/${.:name} + python_path = ${modconfdir}/${.:name} # # python_path_include_conf_dir:: @@ -104,7 +104,7 @@ python { # # func_detach = detach -# func_authorize = authorize + func_authorize = authorize # func_authenticate = authenticate # func_preacct = preacct # func_accounting = accounting diff --git a/raddb/sites-available/default b/raddb/sites-available/default index 6a0a596794..9d87df39dc 100644 --- a/raddb/sites-available/default +++ b/raddb/sites-available/default @@ -1,869 +1,18 @@ -# -*- text -*- -# -# -# $Id$ - -####################################################################### -# -# = The default Virtual Server -# -# The `default` virtual server is the first one that is enabled on a -# default installation of FreeRADIUS. This configuration is -# designed to work in the widest possible set of circumstances, with -# the widest possible number of authentication methods. This means -# that in general, you should need to make very few changes to this -# file. -# -# The usual approach is as follows: -# -# * configure users in a database (e.g. the `files` module, or in -# `sql`) -# * configure the relevant module to talk to the database -# (e.g. `sql`) -# * If using EAP / 802.1X, configure the certificates in -# the `certs/` directory. -# -# Then, run the server. This process will ensure that users can log -# in via PAP, CHAP, MS-CHAP, etc. You should so test the server via -# `radtest` to verify that it works. -# -# ## Editing this file -# -# Please read "man radiusd" before editing this file. See the -# section titled DEBUGGING. It outlines a method where you can -# quickly obtain the configuration you want, without running into -# trouble. See also "man unlang", which documents the format of this -# file. And finally, the debug output can be complex. Please read -# https://wiki.freeradius.org/radiusd-X to understand that output. -# -# The best way to configure the server for your local system is to -# *carefully* edit this file. Most attempts to make large edits to -# this file will *break the server*. Any edits should be small, and -# tested by running the server with `radiusd -X`. Once the edits -# have been verified to work, save a copy of these configuration -# files somewhere. We recommend using a revision control system such -# as `git`, or even a "tar" file. Then, make more edits, and test, -# as above. -# -# There are many "commented out" references to modules and -# configurations These references serve as place-holders, and as -# documentation. If you need the functionality of that module, then: -# -# * configure the module in `mods-available/` -# * enable the module in `mods-enabled`. e.g. for LDAP, do: `cd mods-enabled;ln -s ../mods-available/ldap` -# * uncomment the references to it in this file. -# -# In most cases, those small changes will result in the server being -# able to connect to the database, and to authenticate users. - -# ## The Virtual Server -# -# This is the `default` virtual server. -# server default { - # - # namespace:: - # - # In v4, all "server" sections MUST start with a "namespace" - # parameter. This tells the server which protocol is being used. - # - # All of the "listen" sections in this virtual server will - # only accept packets for that protocol. - # namespace = radius - - # - # ### The listen section - # - # The `listen` sections in v4 are very different from the - # `listen sections in v3. The changes were necessary in - # order to make FreeRADIUS more flexible, and to make the - # configuration simpler and more consistent. - # listen { - # - # type:: The type of packet to accept. - # - # Multiple types can be accepted by using multiple - # lines of `type = ...`. - # - # This change from v3 makes it much clearer what kind - # of packet is being accepted. The old `auth+acct` - # configuration was awkward and potentially - # confusing. - # type = Access-Request - type = Status-Server - - # - # transport:: The transport protocol. - # - # The allowed transports for RADIUS are currently - # `udp` and `tcp`. A `listen` section can only have - # one `transport` defined. For multiple transports, - # use multiple `listen` sections. - # - # You can have a "headless" server by commenting out - # the "transport" configuration. A "headless" server - # will process packets from other virtual servers, - # but will not accept packets from the network. - # - # The `inner-tunnel` server is an example of a - # headless server. It accepts packets from the - # "inner tunnel" portion of PEAP and TTLS. But it - # does not accept those packets from the network. - # transport = udp - - # - # limit:: limits for this socket. - # - # The `limit` section contains configuration items - # which enforce various limits on the socket. These - # limits are usually transport-specific. - # - # Limits are used to prevent "run-away" problems. - # - limit { - # - # max_clients:: The maximum number of dynamic - # clients which can be defined for this - # listener. - # - # If dynamic clients are not used, then this - # configuration item is ignored. - # - # The special value of `0` means "no limit". - # We do not recommend using `0`, as attackers - # could forge packets from the entire - # Internet, and cause FreeRADIUS to run out - # of memory. - # - # This configuration item should be set to - # the number of individual RADIUS clients - # (e.g. NAS, AP, etc.) which will be sending - # packets to FreeRADIUS. - # - max_clients = 256 - - # - # max_connections:: The maximum number of - # connected sockets which will be accepted - # for this listener. - # - # Each connection opens a new socket, so be - # aware of system file descriptor - # limitations. - # - # If the listeners do not use connected - # sockets (e.g. TCP), then this configuration - # item is ignored. - # - max_connections = 256 - - # - # idle_timeout:: Time after which idle - # connections or dynamic clients are deleted. - # - # Useful range of values: 5 to 600 - # - idle_timeout = 60.0 - - # - # nak_lifetime:: Time for which blocked - # clients are placed into a NAK cache. - # - # If a dynamic client is disallowed, it is - # placed onto a "NAK" list for a period - # of time. This process helps to prevent - # DoS attacks. When subsequent packets are - # received from that IP address, they hit the - # "NAK" cache, and are immediately discarded. - # - # After `nak_timeout` seconds, the blocked - # entry will be removed, and the IP will be - # allowed to try again to define a dynamic - # client. - # - # Useful range of values: 1 to 600 - # - nak_lifetime = 30.0 - - # - # cleanup_delay:: The time to wait (in - # seconds) before cleaning up a reply to an - # `Access-Request` packet. - # - # The reply is normally cached internally for - # a short period of time, after it is sent to - # the NAS. The reply packet may be lost in - # the network, and the NAS will not see it. - # The NAS will then resend the request, and - # the server will respond quickly with the - # cached reply. - # - # If this value is set too low, then - # duplicate requests from the NAS MAY NOT be - # detected, and will instead be handled as - # separate requests. - # - # If this value is set too high, then the - # server will use more memory for no benefit. - # - # This value can include a decimal number of - # seconds, e.g. "4.1". - # - # Useful range of values: 2 to 30 - # - cleanup_delay = 5.0 - } - - # - # #### UDP Transport - # - # When the `listen` section contains `transport = - # udp`, it looks for a "udp" subsection. This - # subsection contains all of the configuration for - # the UDP transport. - # udp { - # - # ipaddr:: The IP address where FreeRADIUS - # accepts packets. - # - # The address can be IPv4, IPv6, a numbered - # IP address, or a host name. If a host name - # is used, the IPv4 address is preferred. - # When there is no IPv4 address for a host - # name, the IPv6 address is used. - # - # As with UDP, `ipaddr`, `ipv4addr`, and `ipv6addr` - # are all allowed. - # - # ipv4addr:: Use IPv4 addresses. - # - # The same as `ipaddr`, but will only use - # IPv4 addresses. - # - # ipv6addr:: Use IPv6 addresses. - # - # The same as `ipaddr`, but will only use - # IPv6 addresses. - # ipaddr = * - - # - # port:: the UDP where FreeRADIUS accepts - # packets. - # - # The default port for Access-Accept packets - # is `1812`. - # port = 1812 - - # - # dynamic_clients:: Whether or not we allow - # dynamic clients. - # - # If set to `true`, then packets from unknown - # clients are passed through the `new - # client` subsection below. See that section - # for more information about how dynamic - # clients work. - # -# dynamic_clients = true - - # - # networks:: The list of networks which are - # allowed to send packets to FreeRADIUS for - # dynamic clients. - # - # If there are no dynamic clients, then this - # section is ignored. - # - # The purpose of the `networks` subsection is - # to ensure that only a small set of source - # IPs can trigger dynamic clients. If anyone - # could trigger dynamic clients, then the - # server would be subject to a DoS attack. - # - networks { - # - # allow:: Allow packets from these - # networks to define dynamic clients. - # - # Packets from all other sources will - # be rejected. - # - # When a packet is from an allowed - # network, it will be run through the - # `new client` subsection below. - # That subsection can still reject - # the client request. - # - # There is no limit to the number of - # networks which can be listed here. - # - allow = 127/8 - allow = 192.0.2/24 - - # - # deny:: deny some networks. - # - # The default behavior is to only - # allow packets from the `allow` - # networks. The `deny` directive - # allows you to carve out a subset of - # an `allow` network, where some - # packets are denied. - # - # That is, a `deny` network MUST - # exist within a previous `allow` network. - # - # The `allow` and `deny` rules apply - # only to networks. The order which - # they appear in the configuration - # file does not matter. - # -# deny = 127.0.0/24 - } } - - # - # #### TCP Transport - # - # When the configuration has `transport = tcp`, it - # looks for a `tcp` subsection. That subsection - # contains all of the configuration for the TCP - # transport. - # - # Since UDP and TCP are similar, the majority of the - # configuration items are the same for both of them. - # - tcp { - # - # ipaddr:: The IP address where FreeRADIUS - # accepts packets. - # - # It has the same definition and meaning as - # the UDP `ipaddr` configuration above. - # - ipaddr = * - - # - # NOTE: As with v3, `ipaddr`, `ipv4addr`, and `ipv6addr` - # are all allowed. - # - - # - # port:: the TCP where FreeRADIUS accepts - # packets. - # - # The default port for Access-Accept packets - # is `1812`. - # - port = 1812 - - # - # dynamic_clients:: Whether or not we allow dynamic clients. - # - # If set to true, then packets from unknown - # clients are passed through the "new client" - # subsection below. See that section for - # more information. - # -# dynamic_clients = true - - # - # networks { ... }:: - # - # If dynamic clients are allowed, then limit - # them to only a small set of source - # networks. - # - # If dynamic clients are not allowed, then - # this section is ignored. - # - networks { - # - # allow:: Allow packets from a network. - # - # deny:: Deny packets from a network. - # - # Allow or deny packets from these networks - # to define dynamic clients. - # - # Packets from all other sources will - # be discarded. - # - # Even if a packet is from an allowed - # network, it still must be permitted - # by the "new client" subsection. - # - # There is no limit to the number of - # networks which can be listed here. - # - # The allow / deny checks are organised by - # address. The order of the items given here - # does not matter. - # - allow = 127/8 - allow = 192.0.2/24 -# deny = 127.0.0/24 - } - } - - # - # #### Access-Request subsection - # - # This section contains configuration which is - # specific to processing `Access-Request` packets. - # - # Similar sections can be added, but are not - # necessary for Accounting-Request (and other) - # packets. At this time, there is no configuration - # needed for other packet types. - # - Access-Request { - # - # log:: Logging configuration for `Access-Request` packets - # - # In v3, the `Access-Request` logging was - # configured in the main `radiusd.conf` file, - # in the main `log` subsection. That - # limitation meant that the configuration was - # global to FreeRADIUS. i.e. you could not - # have different `Access-Request` logging for - # different virtual server. - # - # The extra configuration in v4 allows for - # increased flexibility. - # - log { - # - # stripped_names:: Log the full - # `User-Name` attribute, as it was - # found in the request. - # - # allowed values: {no, yes} - # - stripped_names = no - - # - # auth:: Log authentication requests - # to the log file. - # - # allowed values: {no, yes} - # - auth = no - - # - # auth_goodpass:: Log "good" - # passwords with the authentication - # requests. - # - # allowed values: {no, yes} - # - auth_goodpass = no - - # - # auth_badpass:: Log "bad" - # passwords with the authentication - # requests. - # - # allowed values: {no, yes} - # - auth_badpass = no - - # - # msg_goodpass:: - # msg_badpass:: - # - # Log additional text at the end of the "Login OK" messages. - # for these to work, the "auth" and "auth_goodpass" or "auth_badpass" - # configurations above have to be set to "yes". - # - # The strings below are dynamically expanded, which means that - # you can put anything you want in them. However, note that - # this expansion can be slow, and can negatively impact server - # performance. - # -# msg_goodpass = "" -# msg_badpass = "" - - # - # msg_denied:: - # - # The message when the user exceeds the Simultaneous-Use limit. - # - msg_denied = "You are already logged in - access denied" - } - - # - # session:: Controls how ongoing - # (multi-round) sessions are handled - # - # This section is primarily useful for EAP. - # It controls the number of EAP - # authentication attempts that can occur - # concurrently. - # - session { - # - # max:: The maximum number of ongoing sessions - # -# max = 4096 - - # - # timeout:: How long to wait before expiring a - # session. - # - # The timer starts when a response - # with a state value is sent. The - # timer stops when a request - # containing the previously sent - # state value is received. - # -# timeout = 15 - } - } - } - - listen { - type = Access-Request - type = Status-Server - - transport = tcp - - tcp { - # - # As with v3, "ipaddr", "ipv4addr", and "ipv6addr" - # are all allowed. - # - ipaddr = * - port = 1812 - - # - # Whether or not we allow dynamic clients. - # - # If set to true, then packets from unknown - # clients are passed through the "new client" - # subsection below. See that section for - # more information. - # -# dynamic_clients = true - - # - # If dynamic clients are allowed, then limit - # them to only a small set of source - # networks. - # - # If dynamic clients are not allowed, then - # this section is ignored. - # - networks { - # - # Allow packets from these networks - # to define dynamic clients. - # - # Packets from all other sources will - # be rejected. - # - # Even if a packet is from an allowed - # network, it still must be allowed - # by the "new client" subsection. - # - # There is no limit to the number of - # networks which can be listed here. - # - allow = 127/8 - allow = 192.0.2/24 -# deny = 127.0.0/24 - } - } - - # - # #### Access-Request subsection - # - # This section contains configuration which is - # specific to processing `Access-Request` packets. - # - # Similar sections can be added, but are not - # necessary for Accounting-Request (and other) - # packets. At this time, there is no configuration - # needed for other packet types. - # - Access-Request { - # - # log:: Logging configuration for `Access-Request` packets - # - # In v3, the `Access-Request` logging was - # configured in the main `radiusd.conf` file, - # in the main `log` subsection. That - # limitation meant that the configuration was - # global to FreeRADIUS. i.e. you could not - # have different `Access-Request` logging for - # different virtual server. - # - # The extra configuration in v4 allows for - # increased flexibility. - # - log { - # stripped_names:: Log the full - # `User-Name` attribute, as it was - # found in the request. - # - # allowed values: {no, yes} - # - stripped_names = no - - # auth:: Log authentication requests - # to the log file. - # - # allowed values: {no, yes} - # - auth = no - - # auth_goodpass:: Log "good" - # passwords with the authentication - # requests. - # - # allowed values: {no, yes} - # - auth_badpass = no - - # auth_badpass:: Log "bad" - # passwords with the authentication - # requests. - # - # allowed values: {no, yes} - # - auth_goodpass = no - - # Log additional text at the end of the "Login OK" messages. - # for these to work, the "auth" and "auth_goodpass" or "auth_badpass" - # configurations above have to be set to "yes". - # - # The strings below are dynamically expanded, which means that - # you can put anything you want in them. However, note that - # this expansion can be slow, and can negatively impact server - # performance. - # -# msg_goodpass = "" -# msg_badpass = "" - - # The message when the user exceeds the Simultaneous-Use limit. - # - msg_denied = "You are already logged in - access denied" - } - - # - # session:: Controls how ongoing - # (multi-round) sessions are handled - # - # This section is primarily useful for EAP. - # It controls the number of EAP - # authentication attempts that can occur - # concurrently. - # - session { - # - # max:: The maximum number of ongoing sessions - # -# max = 4096 - - # timeout:: How long to wait before expiring a - # session. - # - # The timer starts when a response - # with a state value is sent. The - # timer stops when a request - # containing the previously sent - # state value is received. - # -# timeout = 15 - } - } - } - - # - # ### Listen for Accounting-Request packets - # - listen { - type = Accounting-Request - - transport = udp - - udp { - ipaddr = * - port = 1813 - } - } - - - # - # ### Local Clients - # - # The "client" sections can can also be placed here. Unlike - # v3, they do not need to be wrapped in a "clients" section. - # They can just co-exist beside the "listen" sections. - # - # Clients listed here will apply to *all* listeners in this - # virtual server. - # - # The clients listed here take precedence over the global - # clients. - # - client localhost { - shortname = sample - ipaddr = 192.0.2.1 - secret = testing123 - - # The other "client" configuration items can be added - # here, too. } - -###################################################################### -# -# ## Packet Processing sections -# -# The sections below are called when a RADIUS packet has been -# received. -# -# * recv Access-Request - for authorization and authentication -# * recv Status-Server - for checking the server is responding -# -###################################################################### - -# -# ### Receive Access-Request packets -# -recv Access-Request { - # - # Take a `User-Name`, and perform some checks on it, for - # spaces and other invalid characters. If the `User-Name` - # is invalid, reject the request. - # - # See policy.d/filter for the definition of the - # filter_username policy. - # - filter_username - - # - # Some broken equipment sends passwords with embedded - # zeros, i.e. the debug output will show: - # - # User-Password = "password\000\000" - # - # This policy will fix the password to just be "password". - # -# filter_password - - # - # If you intend to use CUI and you require that the - # Operator-Name be set for CUI generation and you want to - # generate CUI also for your local clients, then uncomment - # operator-name below and set the operator-name for - # your clients in clients.conf. - # -# operator-name - - # - # Proxying example - # - # The following example will proxy the request if the - # username ends in example.com. - # -# if (&User-Name =~ /@example\.com$/) { -# update control { -# &Auth-Type := "proxy-example.com" -# } -# } - - # - # If you want to generate CUI for some clients that do - # not send proper CUI requests, then uncomment cui below - # and set "add_cui = yes" for these clients in - # clients.conf. - # -# cui - - # - # The `auth_log` module will write all `Access-Request` packets to a file. - # - # Uncomment the next bit in order to have a log of - # authentication requests. For more information, see - # `mods-available/detail.log`. - # -# auth_log - - # - # The `chap` module will set `Auth-Type := CHAP` if the - # packet contains a `CHAP-Challenge` attribute. The module - # does this only if the `Auth-Type` attribute has not already - # been set. - # - chap - - # - # The `mschap` module will set `Auth-Type := mschap` if the - # packet contains an `MS-CHAP-Challenge` attribute. The - # module does this only if the `Auth-Type` attribute has not - # already been set. - # - mschap - - # - # The `digest` module implements the SIP Digest - # authentication method. - # - # Note that the module does not implement RFC 4590. Instead, - # it implements an earlier draft of the specification. Since - # all of the NAS equipment also implements the earlier draft, - # this limitation is fine. - # - # If you have a Cisco SIP server authenticating against - # FreeRADIUS, the `digest` module will set `Auth-Type := - # "Digest"` if we are handling a SIP Digest request and the - # `Auth-Type` has not already been set. - # - digest - - # - # The `wimax` module fixes up various WiMAX-specific stupidities. - # - # The WiMAX specification says that the `Calling-Station-Id` - # is 6 octets of the MAC. This definition conflicts with RFC - # 3580, and all common RADIUS practices. Uncommenting the - # `wimax` module here allows the module to change the - # `Calling-Station-Id` attribute to the normal format as - # specified in RFC 3580 Section 3.21. - # -# wimax - - # - # The `eap` module takes care of all EAP authentication, - # including EAP-MD5, EAP-TLS, PEAP and EAP-TTLS. - # - # The module also sets the EAP-Type attribute in the request - # list, to the incoming EAP type. - # - # The `eap` module returns `ok` if it is not yet ready to - # authenticate the user. The configuration below checks for - # that return value, and if so, stops processing the current - # section. - # - # The result is that any LDAP and/or SQL servers will not be - # queried during the initial set of packets that go back and - # forth to set up EAP-TTLS or PEAP. - # - # We also recommend doing user lookups in the `inner-tunnel` - # virtual server. - # - eap { - ok = return + recv Access-Request { + python } +<<<<<<< Updated upstream # # The `unix` module will obtain passwords from `/etc/passwd` @@ -1588,4 +737,6 @@ send Accounting-Response { # attr_filter.accounting_response } +======= +>>>>>>> Stashed changes } diff --git a/src/modules/rlm_python/example.py b/src/modules/rlm_python/example.py index 8ff58f2692..1f37eac1a1 100644 --- a/src/modules/rlm_python/example.py +++ b/src/modules/rlm_python/example.py @@ -12,12 +12,13 @@ def instantiate(p): print(p) # return 0 for success or -1 for failure -def authorize(p): +def authorize(request): print("*** authorize ***") + + freeradius.log('*** log call in authorize ***') print("") - freeradius.log(freeradius.L_INFO, '*** log call in authorize ***') - print("") - print(p) + print(request) + print(request.pairs.request) print("") print(freeradius.config) print("") @@ -30,7 +31,7 @@ def preacct(p): def accounting(p): print("*** accounting ***") - freeradius.log(freeradius.L_INFO, '*** log call in accounting (0) ***') + freeradius.log('*** log call in accounting (0) ***') print("") print(p) return freeradius.RLM_MODULE_OK diff --git a/src/modules/rlm_python/rlm_python.c b/src/modules/rlm_python/rlm_python.c index 400f280bf8..59be71c9cc 100644 --- a/src/modules/rlm_python/rlm_python.c +++ b/src/modules/rlm_python/rlm_python.c @@ -19,9 +19,11 @@ * @file rlm_python.c * @brief Translates requests between the server an a python interpreter. * - * @note Rewritten by Paul P. Komkoff Jr . + * @note Rewritten by Arran Cudbard-Bell (a.cudbardb@freeradius.org) + * very little of the original code remains. * - * @copyright 2000,2006,2015-2016 The FreeRADIUS server project + * @copyright 2020-2021 Arran Cudbard-Bell (a.cudbardb@freeradius.org) + * @copyright 2000,2006,2015-2021 The FreeRADIUS server project * @copyright 2002 Miguel A.L. Paraz (mparaz@mparaz.com) * @copyright 2002 Imperium Technology, Inc. */ @@ -37,9 +39,12 @@ RCSID("$Id$") #include #include +#include + #include /* Python header not pulled in by default. */ #include #include +#include /** Specifies the module.function to load for processing a section * @@ -84,33 +89,486 @@ typedef struct { * thread must have a PyThreadState per interpreter, to track execution. */ typedef struct { - PyThreadState *state; //!< Module instance/thread specific state. + PyThreadState *state; //!< Module instance/thread specific state. } rlm_python_thread_t; +/** Additional fields for pairs + * + */ +typedef struct { + PyObject_HEAD //!< Common fields needed for every python object. + fr_pair_list_t *head; //!< of the pair list. + fr_cursor_t iter; //!< Holds state for the iterator. + bool iter_init; //!< Whether the iterator has been initialised. + tmpl_t *tmpl; //!< Describes which attribute is being accessed. +} py_freeradius_pair_t; + +typedef struct { + py_freeradius_pair_t pair; //!< Fields from the pair struct. + tmpl_pair_list_t list_ref; //!< List this structure represents. +} py_freeradius_pair_list_t; + +typedef struct { + PyObject_HEAD //!< Common fields needed for every python object. + PyObject *request; //!< Request list. + PyObject *reply; //!< Reply list. + PyObject *control; //!< Control list. + PyObject *state; //!< Session state list. +} py_freeradius_pair_root_t; + +typedef struct { + PyObject_HEAD //!< Common fields needed for every python object. + request_t *request; //!< The current request. + PyObject *pairs; //!< Pair root. +} py_freeradius_request_t; + +/** Wrapper around a python instance + * + * This is added to the FreeRADIUS module to allow us to + * get at the global and thread local instance data. + */ +typedef struct { + PyObject_HEAD //!< Common fields needed for every python object. + rlm_python_t *inst; //!< Global python instance. + rlm_python_thread_t *t; //!< Thread-specific python instance. + request_t *request; //!< Current request. +} py_freeradius_state_t; + static void *python_dlhandle; static PyThreadState *global_interpreter; //!< Our first interpreter. static rlm_python_t *current_inst; //!< Used for communication with inittab functions. +static rlm_python_thread_t *current_t; //!< Used for communication with inittab functions. + static CONF_SECTION *current_conf; //!< Used for communication with inittab functions. static char *default_path; //!< The default python path. +static PyObject *py_freeradius_log(UNUSED PyObject *self, PyObject *args, PyObject *kwds); +static int py_freeradius_state_init(PyObject *self, UNUSED PyObject *args, UNUSED PyObject *kwds); +static int py_freeradius_pair_list_init(PyObject *self, PyObject *args, PyObject *kwds); + +static PyObject *py_freeradius_pair_map_subscript(PyObject *self, PyObject *attr); +static int py_freeradius_pair_init(PyObject *self, PyObject *args, PyObject *kwds); + +static void python_error_log(rlm_python_t const *inst, request_t *request); + +/** The class which all pair types inherit from + * + */ +static PyTypeObject py_freeradius_pair_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.Pair", + .tp_doc = "An attribute value pair", + .tp_basicsize = sizeof(py_freeradius_pair_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = PyType_GenericNew, + .tp_init = py_freeradius_pair_init, + +}; + +/** Contains a list of one or more value pairs of a specific type + * + */ +static PyTypeObject py_freeradius_value_pair_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.ValuePairList", + .tp_doc = "A value pair, i.e. one of the type string, integer, ipaddr etc...)", + .tp_basicsize = sizeof(py_freeradius_pair_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_base = &py_freeradius_pair_def, + .tp_new = PyType_GenericNew, + .tp_init = py_freeradius_pair_init, + .tp_as_mapping = &(PyMappingMethods){ + .mp_subscript = py_freeradius_pair_map_subscript + } +}; + +/** Contains group attribute of a specific type + * + * Children of this attribute may be accessed using the map protocol + * i.e. foo['child-of-foo']. + * + */ +static PyTypeObject py_freeradius_grouping_pair_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.GroupingPair", + .tp_doc = "A grouping pair, i.e. one of the type group, tlv, vsa or vendor. " + "Children are accessible via the mapping protocol i.e. foo['child-of-foo]" + .tp_basicsize = sizeof(py_freeradius_pair_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_base = &py_freeradius_pair_def, + .tp_new = PyType_GenericNew, + .tp_init = py_freeradius_pair_init, + .tp_as_mapping = &(PyMappingMethods){ + .mp_subscript = py_freeradius_pair_map_subscript + } +}; + +/** Contains a list of one or more grouping pairs of a specific type + * + * As a convenience children may be accessed directly using the map + * interface, i.e. foo['child-of-foo'], which is equivalent to + * foo[0]['child-of-foo'] similar to attribute reference syntax in unlang. + * This is a bit of a hack, but useful, and comes naturally from the fact + * that GroupingPairList subclasses GroupingPair. + * + * Accessing indexes i.e. foo[n] will return the n'th instance of the + * grouping attribute. + * + */ +static PyTypeObject py_freeradius_grouping_pair_list_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.GroupingPairList", + .tp_doc = "An ordered list of freeradius.GroupingPair objects", + .tp_basicsize = sizeof(py_freeradius_pair_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_base = &py_freeradius_grouping_pair_def, + .tp_new = PyType_GenericNew, + .tp_init = py_freeradius_pair_init, +}; + +/** Each instance contains a top level list (i.e. request, reply, control, session-state) + */ +static PyTypeObject py_freeradius_leagcy_pair_list_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.LegacyPairList", + .tp_doc = "A list of objects of freeradius.GroupingPairList and freeradius.ValuePair", + .tp_basicsize = sizeof(py_freeradius_pair_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_base = &py_freeradius_pair_def, + .tp_new = PyType_GenericNew, + .tp_init = py_freeradius_pair_list_init, /* Does nothing, just stops parent init being called */ + .tp_as_mapping = &(PyMappingMethods){ + .mp_subscript = py_freeradius_pair_map_subscript + } +}; + +static PyMemberDef py_freeradius_pair_root_attrs[] = { + { + .name = "request", + .type = T_OBJECT, + .offset = offsetof(py_freeradius_pair_root_t, request), + .flags = READONLY, + .doc = "Pairs in the request list - received from the network" + }, + { + .name = "reply", + .type = T_OBJECT, + .offset = offsetof(py_freeradius_pair_root_t, reply), + .flags = READONLY, + .doc = "Pairs in the reply list - sent to the network" + }, + { + .name = "control", + .type = T_OBJECT, + .offset = offsetof(py_freeradius_pair_root_t, control), + .flags = READONLY, + .doc = "Pairs in the control list - control the behaviour of subsequently called modules" + }, + { + .name = "session-state", + .type = T_OBJECT, + .offset = offsetof(py_freeradius_pair_root_t, state), + .flags = READONLY, + .doc = "Pairs in the session-state list - persists for the length of the session" + }, + { NULL } /* Terminator */ +}; + +static PyTypeObject py_freeradius_pair_root_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.PairRoot", + .tp_doc = "Root of all pair lists associated with the request", + .tp_basicsize = sizeof(py_freeradius_pair_root_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_members = py_freeradius_pair_root_attrs, + .tp_getattro = PyObject_GenericGetAttr +}; + +static PyMemberDef py_freeradius_request_attrs[] = { + { + .name = "pairs", + .type = T_OBJECT, + .offset = offsetof(py_freeradius_request_t, pairs), + .flags = READONLY, + .doc = "Object providing access to all pair lists associated with the request " + "(.request, .reply, .control, .session-state)" + }, + { NULL } /* Terminator */ +}; + +static PyTypeObject py_freeradius_request_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.Request", + .tp_doc = "freeradius request handle", + .tp_basicsize = sizeof(py_freeradius_request_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_members = py_freeradius_request_attrs +}; + +static PyTypeObject py_freeradius_state_def = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "freeradius.State", + .tp_doc = "Private state data", + .tp_basicsize = sizeof(py_freeradius_state_t), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = py_freeradius_state_init +}; + /* - * As of Python 3.8 the GIL will be per-interpreter - * If there are still issues with CEXTs, - * multiple interpreters and the GIL at that point - * users can build rlm_python against Python 3.8 - * and the horrible hack of using a single interpreter - * for all instances of rlm_python will no longer be - * required. + * radiusd Python functions + */ +static PyMethodDef py_freeradius_methods[] = { + { "log", (PyCFunction)&py_freeradius_log, METH_VARARGS | METH_KEYWORDS, + "freeradius.log(msg[, type, lvl])\n\n" + "Print a message using the freeradius daemon's logging system.\n" + "type should be one of the following constants:\n" + " freeradius.L_DBG\n" + " freeradius.L_INFO\n" + " freeradius.L_WARN\n" + " freeradius.L_ERR\n" + "lvl should be one of the following constants:\n" + " freeradius.L_DBG_LVL_OFF\n" + " freeradius.L_DBG_LVL_1\n" + " freeradius.L_DBG_LVL_2\n" + " freeradius.L_DBG_LVL_3\n" + " freeradius.L_DBG_LVL_4\n" + " freeradius.L_DBG_LVL_MAX\n" + }, + { NULL, NULL, 0, NULL }, +}; + +static PyModuleDef py_freeradius_def = { + PyModuleDef_HEAD_INIT, + .m_name = "freeradius", + .m_doc = "Freeradius python module", + .m_size = -1, + .m_methods = py_freeradius_methods +}; + +/** Return the module instance object associated with the thread state or interpreter state + * + */ +static inline CC_HINT(always_inline) py_freeradius_state_t *rlm_python_state_obj(void) +{ + PyObject *dict; + + dict = PyThreadState_GetDict(); /* If this is NULL, we're dealing with the main interpreter */ + if (!dict) { + PyObject *module; + + module = PyState_FindModule(&py_freeradius_def); + if (unlikely(!module)) return NULL; + + dict = PyModule_GetDict(module); + if (unlikely(!dict)) return NULL; + } + + return (py_freeradius_state_t *)PyDict_GetItemString(dict, "__State"); +} + +/** Return the rlm_python instance associated with the current interpreter + * + */ +static rlm_python_t const *rlm_python_get_inst(void) +{ + py_freeradius_state_t const *p_state; + + p_state = rlm_python_state_obj(); + if (unlikely(!p_state)) return NULL; + + return p_state->inst; +} + +#if 0 +/** Return the rlm_python thread instance associated with the current interpreter + * + */ +static rlm_python_thread_t const *rlm_python_get_thread_inst(void) +{ + py_freeradius_state_t const *p_state; + + p_state = rlm_python_state_obj(); + if (unlikely(!p_state)) return NULL; + + return p_state->t; +} +#endif + +/** Return the request associated with the current thread state + * + */ +static request_t *rlm_python_get_request(void) +{ + py_freeradius_state_t const *p_state; + + p_state = rlm_python_state_obj(); + if (unlikely(!p_state)) return NULL; + + return p_state->request; +} + +/** Set the request associated with the current thread state + * + */ +static void rlm_python_set_request(request_t *request) +{ + py_freeradius_state_t *p_state; + + p_state = rlm_python_state_obj(); + if (unlikely(!p_state)) return; + + p_state->request = request; +} + +/** Allow fr_log to be called from python * - * As Python 3.x module initialisation is significantly - * different than Python 2.x initialisation, - * it'd be a pain to retain the cext_compat for - * Python 3 and as Python 3 users have the option of - * using as version of Python which fixes the underlying - * issue, we only support using a global interpreter - * for Python 2.7 and below. */ +static PyObject *py_freeradius_log(UNUSED PyObject *self, PyObject *args, PyObject *kwds) +{ + static char const *kwlist[] = { "msg", "type", "lvl", NULL }; + char *msg; + int type = L_DBG; + int lvl = L_DBG_LVL_2; + rlm_python_t const *inst = rlm_python_get_inst(); + + if (fr_debug_lvl < lvl) Py_RETURN_NONE; /* Don't bother parsing args */ + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|ii", (char **)((uintptr_t)kwlist), + &msg, &type, &lvl)) Py_RETURN_NONE; + + fr_log(&default_log, type, __FILE__, __LINE__, "rlm_python (%s) - %s", inst->name, msg); + + Py_RETURN_NONE; +} + +static int py_freeradius_state_init(PyObject *self, UNUSED PyObject *args, UNUSED PyObject *kwds) +{ + py_freeradius_state_t *our_self = (py_freeradius_state_t *)self; + rlm_python_t *inst = current_inst; /* Needed for debug messages */ + + fr_assert(current_inst); + + our_self->inst = talloc_get_type_abort(current_inst, rlm_python_t); + our_self->t = current_t ? talloc_get_type_abort(current_t, rlm_python_thread_t) : NULL; /* May be NULL if this is the first interpreter */ + + DEBUG3("Populating __State data with %p/%p", our_self->inst, our_self->t); + + return 0; +} + +/** Returns a freeradius.Pair, either from the parent's cache or by pulling a representation over from C land + * + */ +static PyObject *py_freeradius_pair_map_subscript(PyObject *self, PyObject *attr) +{ + PyObject *args; + PyObject *py_pair; + rlm_python_t const *inst; + + if (DEBUG_ENABLED3) inst = rlm_python_get_inst(); + + DEBUG3("Dynamically instantiating pair"); + args = PyTuple_New(2); + if (unlikely(!args)) { + error: + /* TODO - Raise exception */ + Py_XDECREF(args); + Py_RETURN_NONE; + } + if (PyTuple_SetItem(args, 0, self) != 0) goto error; + if (PyTuple_SetItem(args, 1, attr) != 0) goto error; + + py_pair = PyObject_CallObject((PyObject *)&py_freeradius_pair_def, args); + if (!py_pair) goto error; + + Py_DECREF(args); + + return py_pair; +} + +static int py_freeradius_pair_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + static char const *kwlist[] = { "parent", "ref", NULL }; + + request_t *request = rlm_python_get_request(); + py_freeradius_pair_t *our_self = (py_freeradius_pair_t *)self; + + PyObject *py_parent; + + Py_buffer ref; + + tmpl_rules_t t_rules = { + .disallow_qualifiers = true, + .disallow_filters = true, /* This all has to be handled within python */ + .at_runtime = true, + .prefix = TMPL_ATTR_REF_PREFIX_NO /* No & allowed in fields */ + }; + int inst = -1; + + if (!request) { + /* TODO - Throw exception */ + return -1; + } + + /* + * Parse parent of type 'Pair' and ref which is + * a string. + */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!s*|i", (char **)((uintptr_t)kwlist), + (PyObject *)&py_freeradius_pair_def, &py_parent, &ref, &inst)) return -1; + t_rules.dict_def = request->dict; + + /* + * If the parent is a list, we use is as the + * default. Caller shouldn't be providing + * qualifiers anyway. + */ + if (PyObject_IsInstance(py_parent, (PyObject *)&py_freeradius_leagcy_pair_list_def)) { + t_rules.list_def = ((py_freeradius_pair_list_t *)py_parent)->list_ref; + /* + * If the parent isn't a list, then we use it as + * the nested parent. + */ + } else { + t_rules.attr_parent = tmpl_da(((py_freeradius_pair_t *)py_parent)->tmpl); + } + + tmpl_afrom_attr_substr(NULL, NULL, &our_self->tmpl, + &FR_SBUFF_IN((char const *)ref.buf, ref.len), NULL, &t_rules); + if (!our_self->tmpl) { + /* TODO - Throw exception */ + RPERROR("ref=%.*s is invalid", (int)ref.len, ref.buf); + return -1; + } + + if (inst > 0) { + if (inst > UINT16_MAX) { + /* TODO - Throw exception */ + return -1; + } + + tmpl_attr_set_leaf_num(our_self->tmpl, (uint16_t)inst); + } + + return 0; +} + +static int py_freeradius_pair_list_init(UNUSED PyObject *self, UNUSED PyObject *args, UNUSED PyObject *kwds) +{ + return 0; +} /* * A mapping of configuration file names to internal variables. @@ -145,14 +603,15 @@ static struct { #define A(x) { #x, x }, A(L_DBG) - A(L_WARN) A(L_INFO) - A(L_ERR) A(L_WARN) - A(L_DBG_WARN) - A(L_DBG_ERR) - A(L_DBG_WARN_REQ) - A(L_DBG_ERR_REQ) + A(L_ERR) + A(L_DBG_LVL_OFF) + A(L_DBG_LVL_1) + A(L_DBG_LVL_2) + A(L_DBG_LVL_3) + A(L_DBG_LVL_4) + A(L_DBG_LVL_MAX) A(RLM_MODULE_REJECT) A(RLM_MODULE_FAIL) A(RLM_MODULE_OK) @@ -169,41 +628,11 @@ static struct { { NULL, 0 }, }; -/* - * radiusd Python functions - */ - -/** Allow fr_log to be called from python - * - */ -static PyObject *mod_log(UNUSED PyObject *module, PyObject *args) -{ - int status; - char *msg; - - if (!PyArg_ParseTuple(args, "is", &status, &msg)) { - Py_RETURN_NONE; - } - - fr_log(&default_log, status, __FILE__, __LINE__, "%s", msg); - - Py_RETURN_NONE; -} - -static PyMethodDef module_methods[] = { - { "log", &mod_log, METH_VARARGS, - "freeradius.log(level, msg)\n\n" \ - "Print a message using the freeradius daemon's logging system. level should be one of the\n" \ - "following constants L_DBG, L_WARN, L_INFO, L_ERR, L_DBG_WARN, L_DBG_ERR, L_DBG_WARN_REQ, L_DBG_ERR_REQ\n" - }, - { NULL, NULL, 0, NULL }, -}; - /** Print out the current error * * Must be called with a valid thread state set */ -static void python_error_log(const rlm_python_t *inst, request_t *request) +static void python_error_log(rlm_python_t const *inst, request_t *request) { PyObject *p_type = NULL, *p_value = NULL, *p_traceback = NULL, *p_str_1 = NULL, *p_str_2 = NULL; @@ -223,10 +652,10 @@ static void python_error_log(const rlm_python_t *inst, request_t *request) PyFrameObject *cur_frame = ptb->tb_frame; ROPTIONAL(RERROR, ERROR, "[%ld] %s:%d at %s()", - fnum, - PyUnicode_AsUTF8(cur_frame->f_code->co_filename), - PyFrame_GetLineNumber(cur_frame), - PyUnicode_AsUTF8(cur_frame->f_code->co_name) + fnum, + PyUnicode_AsUTF8(cur_frame->f_code->co_filename), + PyFrame_GetLineNumber(cur_frame), + PyUnicode_AsUTF8(cur_frame->f_code->co_name) ); ptb = ptb->tb_next; @@ -247,12 +676,10 @@ static void mod_vptuple(TALLOC_CTX *ctx, rlm_python_t const *inst, request_t *re { int i; Py_ssize_t tuple_len; - tmpl_t *dst; + tmpl_t *dst; fr_pair_t *vp; - request_t *current = request; - fr_pair_list_t tmp_list; + request_t *current = request; - fr_pair_list_init(&tmp_list); /* * If the Python function gave us None for the tuple, * then just return. @@ -333,7 +760,7 @@ static void mod_vptuple(TALLOC_CTX *ctx, rlm_python_t const *inst, request_t *re continue; } - if (tmpl_request_ptr(¤t, tmpl_request(dst)) < 0) { + if (radius_request(¤t, tmpl_request(dst)) < 0) { ERROR("%s - Attribute name %s.%s refers to outer request but not in a tunnel, skipping...", funcname, list_name, s1); talloc_free(dst); @@ -352,9 +779,8 @@ static void mod_vptuple(TALLOC_CTX *ctx, rlm_python_t const *inst, request_t *re fr_table_str_by_value(fr_tokens_table, op, "="), s2); } - fr_pair_add(&tmp_list, vp); + radius_pairmove(current, vps, &vp, false); } - radius_pairmove(request, vps, &tmp_list, false); } @@ -465,62 +891,82 @@ static int mod_populate_vptuple(rlm_python_t const *inst, request_t *request, Py return 0; } +static inline CC_HINT(always_inline) PyObject *pair_list_alloc(request_t *request, tmpl_pair_list_t list_ref) +{ + PyObject *py_list; + py_freeradius_pair_list_t *our_list; + + py_list = PyObject_CallObject((PyObject *)&py_freeradius_leagcy_pair_list_def, NULL); + if (unlikely(!py_list)) return NULL; + + our_list = (py_freeradius_pair_list_t *)py_list; + our_list->list_ref = list_ref; + our_list->pair.head = tmpl_request_pair_list(request, list_ref); + return py_list; +} + static unlang_action_t do_python_single(rlm_rcode_t *p_result, rlm_python_t const *inst, request_t *request, PyObject *p_func, char const *funcname) { - fr_pair_t *vp; - PyObject *p_ret = NULL; - PyObject *p_arg = NULL; - int tuple_len; - rlm_rcode_t rcode = RLM_MODULE_OK; + PyObject *p_ret = NULL; + PyObject *p_arg = NULL; + + PyObject *py_request; + py_freeradius_request_t *our_request; + + PyObject *py_pair_root; + py_freeradius_pair_root_t *our_pair_root; + + rlm_rcode_t rcode = RLM_MODULE_OK; + + rlm_python_set_request(request); /* - * We will pass a tuple containing (name, value) tuples - * We can safely use the Python function to build up a - * tuple, since the tuple is not used elsewhere. - * - * Determine the size of our tuple by walking through the packet. - * If request is NULL, pass None. + * Instantiate the request */ - tuple_len = 0; - if (request != NULL) { - tuple_len = fr_pair_list_len(&request->request_pairs); + py_request = PyObject_CallObject((PyObject *)&py_freeradius_request_def, NULL); + if (unlikely(!py_request)) { + python_error_log(inst, request); + RETURN_MODULE_FAIL; } + our_request = (py_freeradius_request_t *)py_request; + our_request->request = request; - if (tuple_len == 0) { - Py_INCREF(Py_None); - p_arg = Py_None; - } else { - int i = 0; - if ((p_arg = PyTuple_New(tuple_len)) == NULL) { - rcode = RLM_MODULE_FAIL; - goto finish; - } + /* + * Instantiate the pair root + */ + py_pair_root = PyObject_CallObject((PyObject *)&py_freeradius_pair_root_def, NULL); + if (unlikely(!py_pair_root)) { + req_error: + Py_DECREF(py_request); + python_error_log(inst, request); + RETURN_MODULE_FAIL; + } + our_pair_root = (py_freeradius_pair_root_t *)py_pair_root; + our_request->pairs = py_pair_root; - for (vp = fr_pair_list_head(&request->request_pairs); - vp; - vp = fr_pair_list_next(&request->request_pairs, vp), i++) { - PyObject *pp; + /* + * Create the actual list roots + * This may be removed when we have a single + * pair root as it's not very efficient. + * + * This is the reason we have a pairs object + * above the pair lists. + */ + our_pair_root->request = pair_list_alloc(request, PAIR_LIST_REQUEST); + if (unlikely(!our_pair_root->request)) goto req_error; - /* The inside tuple has two only: */ - if ((pp = PyTuple_New(2)) == NULL) { - rcode = RLM_MODULE_FAIL; - goto finish; - } + our_pair_root->reply = pair_list_alloc(request, PAIR_LIST_REPLY); + if (unlikely(!our_pair_root->reply)) goto req_error; - if (mod_populate_vptuple(inst, request, pp, vp) == 0) { - /* Put the tuple inside the container */ - PyTuple_SET_ITEM(p_arg, i, pp); - } else { - Py_INCREF(Py_None); - PyTuple_SET_ITEM(p_arg, i, Py_None); - Py_DECREF(pp); - } - } - } + our_pair_root->control = pair_list_alloc(request, PAIR_LIST_CONTROL); + if (unlikely(!our_pair_root->control)) goto req_error; + + our_pair_root->state = pair_list_alloc(request, PAIR_LIST_STATE); + if (unlikely(!our_pair_root->state)) goto req_error; /* Call Python function. */ - p_ret = PyObject_CallFunctionObjArgs(p_func, p_arg, NULL); + p_ret = PyObject_CallFunctionObjArgs(p_func, py_request, NULL); if (!p_ret) { python_error_log(inst, request); /* Needs valid thread with GIL */ rcode = RLM_MODULE_FAIL; @@ -564,10 +1010,10 @@ static unlang_action_t do_python_single(rlm_rcode_t *p_result, /* Now have the return value */ rcode = PyLong_AsLong(p_tuple_int); /* Reply item tuple */ - mod_vptuple(request->reply_ctx, inst, request, &request->reply_pairs, + mod_vptuple(request->reply, inst, request, &request->reply_pairs, PyTuple_GET_ITEM(p_ret, 1), funcname, "reply"); /* Config item tuple */ - mod_vptuple(request->control_ctx, inst, request, &request->control_pairs, + mod_vptuple(request, inst, request, &request->control_pairs, PyTuple_GET_ITEM(p_ret, 2), funcname, "config"); } else if (PyNumber_Check(p_ret)) { @@ -585,6 +1031,8 @@ static unlang_action_t do_python_single(rlm_rcode_t *p_result, } finish: + rlm_python_set_request(NULL); + if (rcode == RLM_MODULE_FAIL) python_error_log(inst, request); Py_XDECREF(p_arg); Py_XDECREF(p_ret); @@ -848,25 +1296,100 @@ static char *python_path_build(TALLOC_CTX *ctx, rlm_python_t *inst, CONF_SECTION */ static PyObject *python_module_init(void) { - rlm_python_t *inst = current_inst; - PyObject *module; + rlm_python_t *inst = current_inst; + PyObject *module; + PyObject *p_state; - static struct PyModuleDef py_module_def = { - PyModuleDef_HEAD_INIT, - .m_name = "freeradius", - .m_doc = "freeRADIUS python module", - .m_size = -1, - .m_methods = module_methods - }; + static pthread_mutex_t init_lock = PTHREAD_MUTEX_INITIALIZER; + bool type_ready = false; fr_assert(inst); - module = PyModule_Create(&py_module_def); + /* + * Only allow one thread at a time do the module + * init. This is out of an abundance of caution + * as it's unclear whether the reference counts + * on the various objects are thread safe. + */ + pthread_mutex_lock(&init_lock); + + /* + * The type definitions are global, so we only + * need to call the init functions the first + * pass through. + */ + if (!type_ready) { + /* + * We need to initialise the definitions first + * this fills in any fields we didn't explicitly + * specify, and gets the structures ready for + * use by the python interpreter. + */ + if (PyType_Ready(&py_freeradius_pair_def) < 0) { + error: + abort(); + pthread_mutex_unlock(&init_lock); + python_error_log(inst, NULL); + Py_RETURN_NONE; + } + + if (PyType_Ready(&py_freeradius_leagcy_pair_list_def) < 0) goto error; + + if (PyType_Ready(&py_freeradius_pair_root_def) < 0) goto error; + + if (PyType_Ready(&py_freeradius_request_def) < 0) goto error; + + if (PyType_Ready(&py_freeradius_state_def) < 0) goto error; + + type_ready = true; + } + + /* + * The module is per-interpreter + */ + module = PyModule_Create(&py_freeradius_def); if (!module) { - python_error_log(inst, NULL); - Py_RETURN_NONE; + Py_DECREF(module); + goto error; + } + + /* + * PyModule_AddObject steals ref on success, we we + * INCREF here to give it something to steal, else + * on free the refcount would go negative. + * + * Note here we're creating a new instance of an + * object, not adding the object definition itself + * as there's no reason that a python script would + * ever need to create an instance object. + * + * The instantiation function associated with the + * the __State object takes care of populating the + * instance data from globals and thread-specific + * variables. + */ + p_state = PyObject_CallObject((PyObject *)&py_freeradius_state_def, NULL); + Py_INCREF(&py_freeradius_state_def); + + if (PyModule_AddObject(module, "__State", p_state) < 0) { + Py_DECREF(&py_freeradius_state_def); + Py_DECREF(module); + goto error; } + /* + * For "Pair" we're inserting an object definition + * as opposed to the object instance we inserted + * for inst. + */ + Py_INCREF(&py_freeradius_pair_def); + if (PyModule_AddObject(module, "Pair", (PyObject *)&py_freeradius_pair_def) < 0) { + Py_DECREF(&py_freeradius_pair_def); + Py_DECREF(module); + goto error; + } + pthread_mutex_unlock(&init_lock); + return module; } @@ -925,7 +1448,7 @@ static int python_interpreter_init(rlm_python_t *inst, CONF_SECTION *conf) return 0; } -static void python_interpreter_free(rlm_python_t *inst, PyThreadState *interp) +static void python_interpreter_free(UNUSED rlm_python_t *inst, PyThreadState *interp) { /* * We incremented the reference count earlier @@ -1053,18 +1576,51 @@ static int mod_detach(void *instance) static int mod_thread_instantiate(UNUSED CONF_SECTION const *conf, void *instance, UNUSED fr_event_list_t *el, void *thread) { - PyThreadState *state; - rlm_python_t *inst = instance; - rlm_python_thread_t *this_thread = thread; + rlm_python_t *inst = talloc_get_type_abort(instance, rlm_python_t); + rlm_python_thread_t *t = talloc_get_type_abort(thread, rlm_python_thread_t); + + PyThreadState *t_state; + PyObject *t_dict; + PyObject *p_state; - state = PyThreadState_New(inst->interpreter->interp); - if (!state) { + current_t = t; + + t_state = PyThreadState_New(inst->interpreter->interp); + if (unlikely(!t_state)) { ERROR("Failed initialising local PyThreadState"); return -1; } - DEBUG3("Initialised new thread state %p", state); - this_thread->state = state; + PyEval_RestoreThread(t_state); /* Switches thread state and locks GIL */ + t_dict = PyThreadState_GetDict(); + if (unlikely(!t_dict)) { + ERROR("Failed getting PyThreadState dictionary"); + error: + PyEval_SaveThread(); /* Unlock GIL */ + PyThreadState_Delete(t_state); + + return -1; + } + + /* + * Instantiate a new instance object which captures + * the global and thread instances, and associates + * them with the thread. + */ + p_state = PyObject_CallObject((PyObject *)&py_freeradius_state_def, NULL); + if (unlikely(!p_state)) { + ERROR("Failed instantiating module instance information object"); + goto error; + } + + if (unlikely(PyDict_SetItemString(t_dict, "__State", p_state) < 0)) { + ERROR("Failed setting module instance information in thread dict"); + goto error; + } + + DEBUG3("Initialised PyThreadState %p", t_state); + t->state = t_state; + PyEval_SaveThread(); /* Unlock GIL */ return 0; } @@ -1170,7 +1726,9 @@ module_t rlm_python = { .type = RLM_TYPE_THREAD_SAFE, .inst_size = sizeof(rlm_python_t), + .inst_type = "rlm_python_t", .thread_inst_size = sizeof(rlm_python_thread_t), + .thread_inst_type = "rlm_python_thread_t", .config = module_config, .onload = mod_load, -- 2.47.2