package com.airlock.iam.servicecontainer.app.application.configuration.radius;

import com.airlock.iam.base.misc.log.LogUtil;
import com.airlock.iam.core.misc.authen.Authenticator;
import com.airlock.iam.core.misc.authen.PasswordPolicy;
import com.airlock.iam.core.misc.authen.PasswordService;
import com.airlock.iam.core.misc.authen.PasswordServiceException;
import com.airlock.iam.core.misc.authen.UsernameTransformer;
import com.airlock.iam.core.misc.authen.locking.TemporaryLockingSettings;
import com.airlock.iam.core.misc.persistency.UserPersister;
import com.airlock.iam.core.misc.plugin.config.validation.CharsetValidator;
import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.IntegerProperty;
import com.airlock.iam.plugin.api.annotation.property.LongProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.PluginProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.PluginPropertyValueReader;
import com.airlock.iam.plugin.api.valuereader.StringPropertyValueReader;
import com.airlock.iam.plugin.framework.misc.exception.MandatoryPropertyMissingException;
import com.airlock.iam.servicecontainer.app.application.service.AbstractService;
import com.airlock.iam.servicecontainer.app.application.service.radius.RadiusResponseModifier;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.AuthenticationState;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.RadiusCallerService;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.RadiusPacketHandler;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.RadiusRolesConfiguration;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.RadiusServerConfigInfo;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.RadiusSessionId;
import com.airlock.iam.servicecontainer.app.application.service.radius.service.SsoCredentialRadiusAttribute;
import com.airlock.iam.servicecontainer.app.application.service.radius.wrapper.context.IamRadiusServerContext;
import com.airlock.iam.servicecontainer.app.application.service.radius.wrapper.lib.IamRadiusServerException;
import com.airlock.iam.servicecontainer.app.application.service.radius.wrapper.lib.IamRadiusServerPacket;
import com.airlock.iam.servicecontainer.app.infrastructure.server.ManagableViaGUI;
import com.airlock.iam.servicecontainer.app.infrastructure.webapp.presenter.RadiusServicePresenter;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;

public abstract class RadiusServiceConfigBase extends AbstractService implements ManagableViaGUI, RadiusCallerService {
   protected static final String REPLY_MESSAGE_SETTINGS = "Reply Message Settings";
   protected static final String AUTHENTICATION_SETTINGS = "Optional Authentication Settings";
   private static final long DEFAULT_RETRANSMISSION_INTERVAL_MILLILS = 30000L;
   private static final int SERVER_SOCKET_SO_TIMEOUT = 100;
   private static final ELogger LOG = ELoggerFactory.getLogger(RadiusServiceConfigBase.class);
   private static final int DEFAULT_RADIUS_PORT = 1812;
   private static final int DEFAULT_RADIUS_PORT_OLD = 1645;
   private static final int MIN_PORT = 0;
   private static final int MAX_PORT = 65535;
   @IntegerProperty(
      description = "The port to listen on for the RADIUS service. Typically 1812 or 1645 (older systems) is used.",
      exampleValues = {1812, 1645},
      minValue = 0,
      maxValue = 65535,
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private int port;
   @StringProperty(
      description = "The IP of the interface the Radius server should listen on. This property is optional. If not present, the Radius service listens on all local interfaces.",
      exampleValues = {"192.168.1.13", "127.0.0.1", "localhost"},
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String interfaceIp;
   private static final String INTERFACE_IP_FIELD = "interfaceIp";
   @BooleanProperty(
      description = "If enabled, all clients must include a valid Message-Authenticator attribute in their requests, otherwise the messages will be discarded.\n<p>Note: even when disabled, received requests containing a Message-Authenticator will always be validated.\nAlso, responses from this server will always include the Message-Authenticator attribute.</p>\n<p><b>Warning:</b> when disabled, this server is vulnerable to the BlastRADIUS attack.</p>",
      displayName = "Enforce Message-Authenticator",
      defaultValue = "true",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean enforceMessageAuthenticator;
   @StringProperty(
      description = "The shared secret for the Radius service. The shared secret is used to protect sensitive information being sent from the Radius client to the Radius server. Since charsets on the server and the client may differ, use only ASCII characters (and choose a longer secret).",
      exampleValues = {"secret4th1ss3rv3r", "s843bdfl03h4", "wonttellu"},
      isSensitive = true,
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String sharedSecret;
   @PluginProperty(
      description = "The authenticator to be used by the Radius service. Make sure that the authentication scheme provided by the authenticator can be used via RADIUS. Client certificate authentication, for example, is usually not done via RADIUS. Common examples are password-, token-, and challenge-response-authentication.",
      optional = false
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private Authenticator authenticator;
   @PluginProperty(
      description = "Configures the behavior of <b>temporary</b> user locking. Notice that responses are <b>not</b> delayed like in the login app not to trigger UDP retransmissions but any further request falling into the temporary lock timeout will be refused immediately with a special message.<br/>If enabling the Temporary Locking settings, either a linear or an exponential factor <b>must</b> be provided or it will have no effect at all. Additionally, a user persister <b>must</b> be specified which must provide the <i>number of failed logins</i> and the <i>last login attempt</i>.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private TemporaryLockingSettings temporaryLockingSettings;
   private static final String TEMPORARY_LOCKING_SETTINGS = "temporaryLockingSettings";
   @PluginProperty(
      description = "The user persister used to calculate the temporary locking and to read out roles after a mandatory password change.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private UserPersister userPersister;
   private static final String USER_PERSISTER_FIELD = "userPersister";
   @PluginProperty(
      description = "Check user authorizations, i.e. check if the user may access the requested service or not. If not configured, no authorization checks are performed.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private RadiusAuthorizationConfig authorizationSettings;
   @StringProperty(
      description = "The charset to be used to decode the password. Leave empty to use the JVM default charset.",
      suggestedValues = {"UTF-8", "ISO-8859-1"},
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String charsetForPassword;
   private static final String CHARSET_FOR_PASSWORD_FIELD = "charsetForPassword";
   @BooleanProperty(
      description = "<p>Certain authenticators support asynchronous authentication requests. That is, instead of a final result like accept or reject, an 'authentication pending' result is returned and the caller (in this case the radius service) must call the authenticator repeatedly to get a final result.</p> <p>If this flag is enabled, the radius service performs the polling and blocks the response until a final result is available. If the flag is disabled, a response is immediately returned to the radius client, asking for a fake challenge (see property 'asynchronousReplyMessage'). When the challenge is returned (content is ignored), the authenticator is queried again and so on.</p><p><b>Note:</b> If the radius service is blocking, the UDP timeout on the client side must be configured to be at least as long as the authenticator timeout.</p>",
      optional = true,
      defaultValue = "true"
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private boolean blockingIfAsynchronous;
   private static final String BLOCKING_IF_ASYNCHRONOUS_DISPLAY_NAME = "Blocking If Asynchronous";
   @IntegerProperty(
      description = "If \"Blocking If Asynchronous\" is TRUE, how long should the radius service pause between polling the authenticator for a new status.",
      exampleValues = {1000, 5000},
      minValue = 10,
      defaultValue = 5000,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private int authenticatorPollingIntervalMillis;
   @IntegerProperty(
      description = "If \"Blocking If Asynchronous\" is TRUE, how long should the radius services keep polling the authenticator before returning a failure.",
      exampleValues = {20, 60},
      minValue = 1,
      defaultValue = 60,
      optional = true,
      displayName = "Authenticator Polling Timeout [s]"
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private int authenticatorPollingTimeoutSecs;
   private static final String AUTHENTICATOR_POLLING_TIMEOUT_DISPLAY_NAME = "Authenticator Polling Timeout [s]";
   @BooleanProperty(
      description = "If Airlock 2FA is used and \"Blocking If Asynchronous\" is enabled, fall back to Passcode after \"Authenticator Polling Timeout [s]\".\nOtherwise, the authentication is cancelled after the timeout.",
      defaultValue = "true",
      optional = true,
      displayName = "Airlock 2FA Passcode Fallback"
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private boolean airlock2FAPasscodeFallback;
   @StringProperty(
      description = "If this attribute is set, the RADIUS server includes the plain password credential in an attribute in the RADIUS Access-Accept response when authentication succeeded.<br/> The corresponding RADIUS attribute is used to transport the password to the RADIUS client. (The \"Class\" attribute has id 25, the \"Filter-Id\" attribute has id 11). Leave this property empty (or do not define the property) to turn this feature off. <p><b>CAUTION</b>: If the feature is used, the password is sent <em>in plaintext</em> to the RADIUS client. This may be a security risk depending on the setup. </p>",
      allowedValues = {"Class", "Filter-Id"},
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String ssoAttribute;
   @PluginProperty(
      description = "Use this property to enable returning of user roles with ACCEPT messages. This enables the Radius client to enforce authorization decisions made by Airlock IAM. If the property is undefined, no roles are returned.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private RadiusRolesConfiguration radiusRolesConfiguration;
   @BooleanProperty(
      description = "If enabled, all RADIUS requests are logged together with the client IP, NAS-Identifier and username. The log is written with INFO level.",
      optional = true,
      defaultValue = "false"
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean logRadiusRequests;
   @IntegerProperty(
      description = "Determines the maximum number of open authentication sessions that are kept in the Radius server. <p> This value should be increased in high-traffic situations if authentication sessions are lost.</p>",
      defaultValue = 4096,
      exampleValues = {1024, 4096, 10240},
      minValue = 10,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int sessionTableSize;
   @IntegerProperty(
      description = "Determines the maximum number of handled packet that are kept in the Radius server in order to detect retransmissions of packets. <p> This value should be increased in high-traffic situations when retransmitted packets are not detected and requests are therefore answered twice. </p>",
      defaultValue = 1024,
      exampleValues = {1024, 4096, 10240},
      minValue = 10,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int retransmissionTableSize;
   @LongProperty(
      description = "Determines the maximum number of milliseconds between two identical looking requests such that they are still considered to be the same request (retransmission). <p> This value should be increased when a Radius client sends retransmissions after more than the indicated time. The value should be lowered if Radius requests are ignored because identical requests are sent within the indicated amount of milliseconds.</p>",
      defaultValue = 30000L,
      exampleValues = {1000L, 10000L, 30000L},
      minValue = 1L,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private long retransmissionIntervalMillis;
   @IntegerProperty(
      description = "Determines the maximum size of a received UDP packet in bytes. <p>\tThis value should be increased if you experience problems because receiving only parts extraodinaryly long Radius packets. </p>",
      defaultValue = 8192,
      exampleValues = {4096, 8192, 16384},
      minValue = 1,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private int packetBufferSize;
   @BooleanProperty(
      description = "Specifies if the RSA/ACE compatibility mode should be used or not. If set to \"TRUE\", this server behaves like a RSA/ACE service would (important for some clients to distinguish next-token-mode from new-pin-mode).<br/> When this mode is enabled, only authenticators that return ACE-like responses can be used. It can - for example - be used in combination with challenge-response authenticators.",
      defaultValue = "false",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private boolean useRsaAceCompatibilityMode;
   @StringProperty(
      description = "<p>Allows definition of a static test user for external monitoring of the Radius service. All login attempts with the static test user are rejected immediately without generating logfile entries. Even if the log level is set to DEBUG and option \"logRadiusRequests\" is enabled, requests with the static test user will not be logged.</p><p><b>Note:</b> The static test user name must not coincide with an existing user name. Otherwise, the corresponding user will not be able to log in.</p> ",
      optional = true,
      exampleValues = "_RejectedUser",
      minLength = 4
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private String staticRejectedUser;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Accept response.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Accept responses.",
      exampleValues = {"Authentication successful", "Access granted"},
      defaultValue = "Login successful.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String accessAcceptReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Accept response after the password has been changed successfully.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, property \"Access Accept Reply Message\" will be included in Access-Accept responses.",
      exampleValues = {"Authentication after password change successful", "Access after password change granted"},
      defaultValue = "Login and password change successful.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String accessAcceptPasswordChangedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Denied responses.",
      exampleValues = {"Authentication failed", "Access denied"},
      defaultValue = "Login failed.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String accessDeniedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response because the user account is locked.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, the general access denied message (see separate property) is used.",
      exampleValues = {"Account locked.", "Your account has been locked for security reasons. Please contact our hotline."},
      defaultValue = "Your user account is locked.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String userLockedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response because the user account is temporarily locked. This only happens if Temporary Locking settings and the user persister are configured.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, the general access denied message (see separate property) is used.",
      exampleValues = "Account temporarily locked; please try again in a few minutes.",
      defaultValue = "Your user account has been locked temporarily. Please try again in a few minutes.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String userTemporarilyLockedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response because the user is not authorized (not enough rights).<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in the responses.",
      exampleValues = {"Not authorized.", "Not enough rights"},
      defaultValue = "Access denied. Not enough access rights.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String notAuthorizedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when in next-token mode.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"Enter next token", "Please wait for next token and enter it"},
      defaultValue = "Please wait for the NEXT token and enter it.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String nextTokenModeReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when in new-pin mode.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"Set new PIN", "Please set a new PIN"},
      defaultValue = "Please choose a new PIN.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String newPinReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when in pin-accepted state.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"PIN Accepted. Enter next token", "PIN changed. Please wait for next token and enter it"},
      defaultValue = "PIN accepted. Please wait for the NEXT token and enter it.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String pinAcceptedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when a token is required. Use the variable ${LAST_USED_TOKEN} to include the last used token (may be an empty string!).<br/>Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"Please enter next token. Last used token: ${LAST_USED_TOKEN}", "Enter token"},
      defaultValue = "Please enter next token.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String tokenRequiredReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response when a required credential is not assigned to the user. If not set, no reply message will be included in the Access-Denied response.",
      exampleValues = "No token has been assigned to your account. Please contact the hotline.",
      defaultValue = "No authentication token has been assigned to your account.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String credentialUnassignedReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when an index challenge is sent. This is the case when the underlying authenticator asks for a specific token and references it by an index. Use the variable ${INDEX} to include the index number in the message.<br/>Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"Please enter token at position ${INDEX}", "Enter TAN at index ${INDEX}"},
      defaultValue = "Please enter token at position ${INDEX}.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String indexChallengeReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when a matrix challenge is sent. This is the case when the underlying authenticator asks for a specific token and references it by one or more coordinate pairs. Use the variable ${CHALLENGE_COORDINATES} to include the coordinate pair(s) in the message. <br/>Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = "Please enter tokens ${CHALLENGE_COORDINATES}",
      defaultValue = "Please enter token(s) ${CHALLENGE_COORDINATES}.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String matrixChallengeReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when asking to set a new password.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"Set a new password:", "Your password has expired, please choose a new password: "},
      defaultValue = "Please choose a new password.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String changePasswordReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Challenge response when asking to retype (confirm) the new password.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Challenge responses.",
      exampleValues = {"Re-type the new password:", "Please re-type the new password to confirm it: "},
      defaultValue = "Please enter the new password again for confirmation.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String confirmPasswordReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response when the new password and its confirmation do not match.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Denied responses.",
      exampleValues = {"Passwords do not match. Login again.", "The new password did not match its confirmation. Please login again."},
      defaultValue = "The passwords do not match. Please login again.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String passwordsDoNotMatchReplyMessage;
   @StringProperty(
      description = "Specifies the reply message sent with an Access-Denied response when the new password could not be accepted because of password policy violations.<br/> Some RADIUS clients (such as keyboard-interactive authentication) may display this to the user. Other clients may make their behavior dependent on this message. <p/>If not set, no reply message will be included in Access-Denied responses.",
      defaultValue = "The new password has not been accepted because it violates the password policy.",
      exampleValues = {"Password not accepted.", "The new password could not be accepted because it does not meet the requirements for new passwords."},
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String passwordNotAcceptedReplyMessage;
   @StringProperty(
      description = "If property \"Blocking If Asynchronous\" is FALSE, the service forwards 'authentication pending' results directly to the radius client in form of a challenge response. This is the message that is displayed to the user in that case. The actual reply to the challenge is ignored and the state of the authentication is checked again.",
      exampleValues = "Please proceed authentication on the mobile phone and press the login button when finished.",
      defaultValue = "Please proceed authentication on your authentication device and press the login button when finished.",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String asynchronousReplyMessage;
   @PluginListProperty(
      displayName = "Username Transformation",
      description = "Username transformers may transform the name received in a Radius request into the single unique user ID required for the authentication process. <br />The transformation of a username takes place before the authenticator reads the user from persistency layer. Transfomers can be chained, i.e. a first transformer could normalize the original name, after which the next transformer looks up the normalized name in a database for eventual transformation matches. <br />In addition to the above description of chaining, a transformer can also signal that it already found the final user ID and that the transformation must stop here.<br/>For further details please refer to the documentation of the username transformer plugins. ",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private List<UsernameTransformer> usernameTransformers;
   @StringProperty(
      description = "Message displayed when the user is required to select a token from the given list.",
      exampleValues = "Please choose:",
      defaultValue = "Please choose:",
      optional = true
   )
   @GroupProperty(groupIdentifier = "Reply Message Settings")
   private String tokenSelectionChoiceMessage;
   @BooleanProperty(description = "Pass on the password to the authenticator as if it is a token code.", optional = true, defaultValue = "false")
   @GroupProperty(groupIdentifier = "Optional Authentication Settings")
   private boolean usePasswordAsToken;
   @PluginListProperty(description = "The accept modifiers may modify the final RADIUS accept packet before it is sent to the client.", optional = true)
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private List<RadiusResponseModifier> acceptModifiers;
   private InetAddress interfaceAddress;
   private Optional<SsoCredentialRadiusAttribute> ssoAttributeObj;
   private DatagramSocket socket;
   private final Object sendLock = new Object();
   private Map<RadiusSessionId, AuthenticationState> sessionTable;
   private Map<String, Long> retransmissionTable;
   private long receivedPackets;
   private final Object receivedPacketsLock = new Object();
   private long handledPackets;
   private final Object handledPacketsLock = new Object();
   private long invalidPackets;
   private final Object invalidPacketsLock = new Object();
   private long retransmittedRequests;
   private final Object retransmittedRequestsLock = new Object();

   protected RadiusServiceConfigBase(
      int port,
      String interfaceIp,
      boolean enforceMessageAuthenticator,
      String sharedSecret,
      Authenticator authenticator,
      TemporaryLockingSettings temporaryLockingSettings,
      UserPersister userPersister,
      RadiusAuthorizationConfig authorizationSettings,
      String charsetForPassword,
      boolean blockingIfAsynchronous,
      int authenticatorPollingIntervalMillis,
      int authenticatorPollingTimeoutSecs,
      boolean airlock2FAPasscodeFallback,
      String ssoAttribute,
      RadiusRolesConfiguration radiusRolesConfiguration,
      boolean logRadiusRequests,
      int sessionTableSize,
      int retransmissionTableSize,
      long retransmissionIntervalMillis,
      int packetBufferSize,
      boolean useRsaAceCompatibilityMode,
      String staticRejectedUser,
      String accessAcceptReplyMessage,
      String accessAcceptPasswordChangedReplyMessage,
      String accessDeniedReplyMessage,
      String userLockedReplyMessage,
      String userTemporarilyLockedReplyMessage,
      String notAuthorizedReplyMessage,
      String nextTokenModeReplyMessage,
      String newPinReplyMessage,
      String pinAcceptedReplyMessage,
      String tokenRequiredReplyMessage,
      String credentialUnassignedReplyMessage,
      String indexChallengeReplyMessage,
      String matrixChallengeReplyMessage,
      String changePasswordReplyMessage,
      String confirmPasswordReplyMessage,
      String passwordsDoNotMatchReplyMessage,
      String passwordNotAcceptedReplyMessage,
      String asynchronousReplyMessage,
      List<UsernameTransformer> usernameTransformers,
      String tokenSelectionChoiceMessage,
      boolean usePasswordAsToken,
      List<RadiusResponseModifier> acceptModifiers
   ) {
      this.port = port;
      this.interfaceIp = interfaceIp;
      this.enforceMessageAuthenticator = enforceMessageAuthenticator;
      this.sharedSecret = sharedSecret;
      this.authenticator = authenticator;
      this.temporaryLockingSettings = temporaryLockingSettings;
      this.userPersister = userPersister;
      this.authorizationSettings = authorizationSettings;
      this.charsetForPassword = charsetForPassword;
      this.blockingIfAsynchronous = blockingIfAsynchronous;
      this.authenticatorPollingIntervalMillis = authenticatorPollingIntervalMillis;
      this.authenticatorPollingTimeoutSecs = authenticatorPollingTimeoutSecs;
      this.airlock2FAPasscodeFallback = airlock2FAPasscodeFallback;
      this.ssoAttribute = ssoAttribute;
      this.radiusRolesConfiguration = radiusRolesConfiguration;
      this.logRadiusRequests = logRadiusRequests;
      this.sessionTableSize = sessionTableSize;
      this.retransmissionTableSize = retransmissionTableSize;
      this.retransmissionIntervalMillis = retransmissionIntervalMillis;
      this.packetBufferSize = packetBufferSize;
      this.useRsaAceCompatibilityMode = useRsaAceCompatibilityMode;
      this.staticRejectedUser = staticRejectedUser;
      this.accessAcceptReplyMessage = accessAcceptReplyMessage;
      this.accessAcceptPasswordChangedReplyMessage = accessAcceptPasswordChangedReplyMessage;
      this.accessDeniedReplyMessage = accessDeniedReplyMessage;
      this.userLockedReplyMessage = userLockedReplyMessage;
      this.userTemporarilyLockedReplyMessage = userTemporarilyLockedReplyMessage;
      this.notAuthorizedReplyMessage = notAuthorizedReplyMessage;
      this.nextTokenModeReplyMessage = nextTokenModeReplyMessage;
      this.newPinReplyMessage = newPinReplyMessage;
      this.pinAcceptedReplyMessage = pinAcceptedReplyMessage;
      this.tokenRequiredReplyMessage = tokenRequiredReplyMessage;
      this.credentialUnassignedReplyMessage = credentialUnassignedReplyMessage;
      this.indexChallengeReplyMessage = indexChallengeReplyMessage;
      this.matrixChallengeReplyMessage = matrixChallengeReplyMessage;
      this.changePasswordReplyMessage = changePasswordReplyMessage;
      this.confirmPasswordReplyMessage = confirmPasswordReplyMessage;
      this.passwordsDoNotMatchReplyMessage = passwordsDoNotMatchReplyMessage;
      this.passwordNotAcceptedReplyMessage = passwordNotAcceptedReplyMessage;
      this.asynchronousReplyMessage = asynchronousReplyMessage;
      this.usernameTransformers = usernameTransformers;
      this.tokenSelectionChoiceMessage = tokenSelectionChoiceMessage;
      this.usePasswordAsToken = usePasswordAsToken;
      this.acceptModifiers = acceptModifiers;
   }

   @Override
   public final String getName() {
      return "Legacy RADIUS Service";
   }

   @Override
   public final String getThreadName() {
      return "Radius";
   }

   public final long numberOfHandledPackets() {
      synchronized (this.handledPacketsLock) {
         return this.handledPackets;
      }
   }

   @Override
   public final void increaseNumberOfHandledPackets() {
      synchronized (this.handledPacketsLock) {
         this.handledPackets++;
      }
   }

   public final long numberOfReceivedPackets() {
      synchronized (this.receivedPacketsLock) {
         return this.receivedPackets;
      }
   }

   public final void increaseNumberOfReceivedPackets() {
      synchronized (this.receivedPacketsLock) {
         this.receivedPackets++;
      }
   }

   public final long numberOfInvalidPackets() {
      synchronized (this.invalidPacketsLock) {
         return this.invalidPackets;
      }
   }

   @Override
   public final void increaseNumberOfInvalidPackets() {
      synchronized (this.invalidPacketsLock) {
         this.invalidPackets++;
      }
   }

   public final long numberOfRetransmittedRequests() {
      synchronized (this.retransmittedRequestsLock) {
         return this.retransmittedRequests;
      }
   }

   public final void increaseNumberOfRetransmittedRequests() {
      synchronized (this.retransmittedRequestsLock) {
         this.retransmittedRequests++;
      }
   }

   @Override
   public final void sendUdpPacket(DatagramPacket packet) throws IOException {
      if (this.socket == null) {
         throw new IllegalStateException("Service not yet started.");
      }

      synchronized (this.sendLock) {
         this.socket.send(packet);
      }
   }

   @ConfigurationValidator
   static void validateConfiguration(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment) throws ConfigurationException {
      checkTemporaryLockingWithPersister(config, env, context);
      checkInterfaceIp(config, env, context, validationEnvironment);
      checkCharsetForPassword(config, env, context);
   }

   private static void checkTemporaryLockingWithPersister(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      PluginConfiguration lockingSettings = PluginPropertyValueReader.effectivePluginPropertyValue(config, "temporaryLockingSettings", env, context);
      PluginConfiguration persister = PluginPropertyValueReader.effectivePluginPropertyValue(config, "userPersister", env, context);
      if (TemporaryLockingSettings.requiresUserPersister(lockingSettings, env, context) && persister == null) {
         throw new MandatoryPropertyMissingException("userPersister", config, env, context);
      }
   }

   private static void checkInterfaceIp(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment valEnv) throws ConfigurationException {
      String interfaceIpString = StringPropertyValueReader.effectiveStringConfigValue(config, "interfaceIp", env, context);
      if (StringUtils.isNotBlank(interfaceIpString) && valEnv == Environment.CURRENT_VM) {
         try {
            InetAddress.getByName(interfaceIpString);
         } catch (UnknownHostException e) {
            throw new PropertyConstraintViolationException("interfaceIp", config, env, context, "Host or IP \"" + interfaceIpString + "\" not valid.", e);
         }
      }
   }

   private static void checkCharsetForPassword(PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context) throws ConfigurationException {
      CharsetValidator.validateOptionalCharsetProperty("charsetForPassword", config, env, context);
   }

   @AfterConfigActivation
   void afterConfigActivation() throws InitializeException {
      IamRadiusServerContext.initializeRadiusServerContext();
      if (this.interfaceIp != null) {
         try {
            this.interfaceAddress = InetAddress.getByName(this.interfaceIp);
         } catch (UnknownHostException e) {
            throw new InitializeException("Invalid interface hostname or IP \"" + this.interfaceIp + "\".", e);
         }
      }

      if (this.sessionTable == null) {
         this.sessionTable = new LinkedHashMap<>(this.sessionTableSize + 13);
      }

      if (this.retransmissionTable == null) {
         this.retransmissionTable = new LinkedHashMap<>(this.retransmissionTableSize + 13);
      }

      this.ssoAttributeObj = Optional.ofNullable(this.ssoAttribute).map(SsoCredentialRadiusAttribute::getFromName);
      if (this.getPasswordService().isPresent() && this.getPasswordPolicy().isPresent()) {
         try {
            this.getPasswordService().get().setPasswordPolicy(this.getPasswordPolicy().get());
         } catch (PasswordServiceException e) {
            throw new InitializeException("Error registering password policies with password service plugin: " + e.getMessage(), e);
         }
      }
   }

   @Override
   public final void run() {
      LOG.info("Service " + this.getName() + " is starting to run");
      this.setRunning(true);
      int currentListenerPort = -1;
      this.socket = null;
      InetAddress currentInetAddress = null;

      try {
         try {
            byte[] receiveBuffer = new byte[this.packetBufferSize];
            DatagramPacket udpPacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
            boolean receiveBufferDirty = false;

            while (!this.isStopping()) {
               synchronized (this.sendLock) {
                  if (currentListenerPort != this.port || currentInetAddress != this.interfaceAddress) {
                     if (this.socket != null) {
                        LOG.info(LogTypes.SERVER_INFO_SYS, "Radius Service stops listening on port " + this.socket.getLocalPort() + " (UDP)");
                        this.socket.close();
                     }

                     if (this.interfaceAddress == null) {
                        this.socket = new DatagramSocket(this.port);
                        LOG.info(
                           LogTypes.SERVER_INFO_SYS, "Radius Service starts listening on port " + this.socket.getLocalPort() + " (UDP) on all local interfaces"
                        );
                     } else {
                        this.socket = new DatagramSocket(this.port, this.interfaceAddress);
                        LOG.info(
                           LogTypes.SERVER_INFO_SYS,
                           "Radius Service starts listening on port "
                              + this.socket.getLocalPort()
                              + " (UDP) on interface \""
                              + this.socket.getLocalAddress()
                              + "\""
                        );
                     }

                     this.socket.setSoTimeout(100);
                     currentListenerPort = this.port;
                     currentInetAddress = this.interfaceAddress;
                  }
               }

               try {
                  if (receiveBufferDirty) {
                     receiveBuffer = new byte[this.packetBufferSize];
                     udpPacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
                     receiveBufferDirty = false;
                  }

                  this.socket.receive(udpPacket);
                  receiveBufferDirty = true;
                  this.increaseNumberOfReceivedPackets();
                  if (LOG.isDebugEnabled()) {
                     LOG.debug("Received UDP packet of size " + udpPacket.getLength() + " from \"" + udpPacket.getAddress() + ":" + udpPacket.getPort() + "\"");
                  }

                  IamRadiusServerPacket radiusPacket;
                  try {
                     radiusPacket = IamRadiusServerContext.parseUdpPacket(
                        udpPacket, this.sharedSecret, this.charsetForPassword(), this.enforceMessageAuthenticator
                     );
                  } catch (IamRadiusServerException | RuntimeException e) {
                     LOG.info(
                        LogTypes.SERVER_INFO_SYS,
                        "Received UDP packet from \""
                           + udpPacket.getAddress()
                           + "\" that is not a valid RADIUS packet ("
                           + e.getMessage()
                           + "). Ignored packet."
                     );
                     this.increaseNumberOfInvalidPackets();
                     continue;
                  }

                  if (LOG.isDebugEnabled()) {
                     LOG.debug("Packet from \"" + udpPacket.getAddress() + ":" + udpPacket.getPort() + "\" is a RADIUS packet: " + radiusPacket.asLogString());
                  }

                  if (this.isRetransmittedRequest(radiusPacket, udpPacket.getAddress(), udpPacket.getPort())) {
                     if (LOG.isDebugEnabled()) {
                        LOG.debug(
                           "Ignoring radius request from \""
                              + udpPacket.getAddress()
                              + ":"
                              + udpPacket.getPort()
                              + "\" because the request is considered to be a retransmitted request and is already being processed."
                        );
                     }

                     this.increaseNumberOfRetransmittedRequests();
                  } else {
                     RadiusPacketHandler task = new RadiusPacketHandler(this, this.toConfigInfo(), radiusPacket, udpPacket.getAddress(), udpPacket.getPort());
                     if (!this.getServer().executeTask(task, "Radius Authentication Service")) {
                        LOG.error(
                           LogTypes.SERVER_INFO_SYS,
                           "Could not obtain worker thread for RADIUS request "
                              + LogUtil.quote(radiusPacket.asLogStringWithoutAttributes())
                              + ". Request has been dropped. Increase Thread-Pool Size in Server to allow for more threads."
                        );
                     }
                  }
               } catch (RuntimeException e) {
                  LOG.info(LogTypes.SERVER_INFO_SYS, "Error handling UDP packet from \"" + udpPacket.getAddress() + "\": " + e.getMessage());
                  LOG.debug("Exception in radius service", e);
               } catch (SocketTimeoutException var37) {
               } catch (IOException e) {
                  LOG.info(
                     LogTypes.SERVER_INFO_SYS,
                     "IO Exception while receiving UDP packet from \"" + udpPacket.getAddress() + "\" (" + e.getMessage() + "). Ignored packet."
                  );
                  this.increaseNumberOfInvalidPackets();
               }
            }
         } finally {
            if (this.socket != null) {
               LOG.info(LogTypes.SERVER_INFO_SYS, "Radius Service stops listening on port " + this.socket.getLocalPort() + " (UDP)");
               synchronized (this.socket) {
                  this.socket.close();
               }
            }
         }
      } catch (IOException e) {
         LOG.error(LogTypes.SERVER_INFO_SYS, "An error occurred while waiting for Radius requests: " + e.getMessage(), e);
      } finally {
         LOG.info("Service " + this.getName() + " has stopped.");
         this.setRunning(false);
         this.setPleaseStopToFalse();
      }
   }

   private RadiusServerConfigInfo toConfigInfo() {
      return RadiusServerConfigInfo.builder()
         .authenticator(this.authenticator)
         .userPersister(Optional.ofNullable(this.userPersister))
         .authorizationConfig(Optional.ofNullable(this.authorizationSettings))
         .temporaryLockingSettings(Optional.ofNullable(this.temporaryLockingSettings))
         .radiusRolesConfiguration(Optional.ofNullable(this.radiusRolesConfiguration))
         .ssoAttribute(this.ssoAttributeObj)
         .passwordService(this.getPasswordService())
         .usernameTransformers(this.usernameTransformers)
         .acceptModifiers(this.acceptModifiers)
         .usePasswordAsToken(this.usePasswordAsToken)
         .isBlockingIfAsynchronous(this.blockingIfAsynchronous)
         .isAirlock2FAPasscodeFallback(this.airlock2FAPasscodeFallback)
         .useRsaAceCompatibilityMode(this.useRsaAceCompatibilityMode)
         .logRadiusRequestsOnInfo(this.logRadiusRequests)
         .authenticatorPollingTimeoutSecs(this.authenticatorPollingTimeoutSecs)
         .authenticatorPollingIntervalMillis(this.authenticatorPollingIntervalMillis)
         .staticRejectedUser(Optional.ofNullable(this.staticRejectedUser))
         .tokenSelectionChoiceMessage(Optional.ofNullable(this.tokenSelectionChoiceMessage))
         .sharedSecret(this.sharedSecret)
         .notAuthorizedReplyMessage(Optional.ofNullable(this.notAuthorizedReplyMessage))
         .accessAcceptReplyMessage(Optional.ofNullable(this.accessAcceptReplyMessage))
         .accessAcceptPasswordChangedReplyMessage(
            Optional.ofNullable(
               StringUtils.isNotBlank(this.accessAcceptPasswordChangedReplyMessage)
                  ? this.accessAcceptPasswordChangedReplyMessage
                  : this.accessAcceptReplyMessage
            )
         )
         .pinAcceptedReplyMessage(Optional.ofNullable(this.pinAcceptedReplyMessage))
         .newPinReplyMessage(Optional.ofNullable(this.newPinReplyMessage))
         .nextTokenModeReplyMessage(Optional.ofNullable(this.nextTokenModeReplyMessage))
         .asynchronousReplyMessage(Optional.ofNullable(this.asynchronousReplyMessage))
         .changePasswordReplyMessage(Optional.ofNullable(this.changePasswordReplyMessage))
         .confirmPasswordReplyMessage(Optional.ofNullable(this.confirmPasswordReplyMessage))
         .passwordNotAcceptedReplyMessage(Optional.ofNullable(this.passwordNotAcceptedReplyMessage))
         .passwordsDoNotMatchReplyMessage(Optional.ofNullable(this.passwordsDoNotMatchReplyMessage))
         .userLockedReplyMessage(Optional.ofNullable((String)StringUtils.defaultIfBlank(this.userLockedReplyMessage, this.accessDeniedReplyMessage)))
         .userTemporarilyLockedReplyMessage(
            Optional.ofNullable((String)StringUtils.defaultIfBlank(this.userTemporarilyLockedReplyMessage, this.accessDeniedReplyMessage))
         )
         .credentialUnassignedReplyMessage(Optional.ofNullable(this.credentialUnassignedReplyMessage))
         .accessDeniedReplyMessage(Optional.ofNullable(this.accessDeniedReplyMessage))
         .tokenRequiredReplyMessage(Optional.ofNullable(this.tokenRequiredReplyMessage))
         .indexChallengeReplyMessage(Optional.ofNullable(this.indexChallengeReplyMessage))
         .matrixChallengeReplyMessage(Optional.ofNullable(this.matrixChallengeReplyMessage))
         .serverPort(this.port)
         .build();
   }

   @Override
   public final void registerSession(RadiusSessionId radiusSessionId, AuthenticationState authState) {
      synchronized (this.sessionTable) {
         if (this.sessionTable.size() >= this.sessionTableSize) {
            Iterator<RadiusSessionId> iter = this.sessionTable.keySet().iterator();
            if (iter.hasNext()) {
               this.sessionTable.remove(iter.next());
            }
         }

         this.sessionTable.put(radiusSessionId, authState);
      }
   }

   @Override
   public final AuthenticationState authenticationState(RadiusSessionId radiusSessionId) {
      if (radiusSessionId == null) {
         throw new IllegalArgumentException("radius state null");
      }

      synchronized (this.sessionTable) {
         return this.sessionTable.get(radiusSessionId);
      }
   }

   @Override
   public final void unregisterSession(RadiusSessionId radiusSessionId) {
      if (radiusSessionId != null) {
         synchronized (this.sessionTable) {
            this.sessionTable.remove(radiusSessionId);
         }
      }
   }

   private boolean isRetransmittedRequest(IamRadiusServerPacket radiusPacket, InetAddress sourceAddress, int sourcePort) {
      String signature = sourceAddress.toString() + "|" + sourcePort + "|" + radiusPacket.identifier();
      if (LOG.isDebugEnabled()) {
         LOG.debug("Checking for retransmitted Radius requests with request signature \"" + signature + "\"");
      }

      long now = System.currentTimeMillis();
      if (this.retransmissionTable == null) {
         throw new IllegalStateException("Config not yet activated");
      }

      Long procTime = this.retransmissionTable.get(signature);
      if (procTime == null) {
         if (this.retransmissionTable.size() >= this.retransmissionTableSize) {
            if (LOG.isDebugEnabled()) {
               LOG.debug("Retransmission table is getting to big (" + this.retransmissionTable.size() + "). Shortening by one.");
            }

            Iterator<String> iter = this.retransmissionTable.keySet().iterator();
            if (iter.hasNext()) {
               this.retransmissionTable.remove(iter.next());
            }
         }

         if (LOG.isDebugEnabled()) {
            LOG.debug("Appended Radius request with signature \"" + signature + "\" into retransmission table");
         }

         this.retransmissionTable.put(signature, now);
      } else {
         if (now - procTime < this.retransmissionIntervalMillis) {
            return true;
         }

         this.retransmissionTable.put(signature, now);
         if (LOG.isDebugEnabled()) {
            LOG.debug("Radius request with \"" + signature + "\" found in transmission table, but too old. Replacing with current request");
         }
      }

      return false;
   }

   abstract Optional<PasswordService> getPasswordService();

   abstract Optional<PasswordPolicy> getPasswordPolicy();

   @Override
   public final Charset charsetForPassword() {
      return StringUtils.isNotBlank(this.charsetForPassword) ? Charset.forName(this.charsetForPassword) : Charset.defaultCharset();
   }

   @Override
   public final Class<RadiusServicePresenter> getPresenterClass() {
      return RadiusServicePresenter.class;
   }

   @SuppressFBWarnings(justification = "generated code")
   public RadiusServiceConfigBase() {
   }
}
