1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-12-22 12:37:58 +01:00

Rework and improve authentication API and internals

Add

- performSaslAnonymousAuthentication()
- performSaslExternalAuthentication(SSLContext)
- addEnabledSaslMechanism(String)
- addEnabledSaslMechanisms(Collection<String>)

to ConnectionConfiguration.Builder.

Instead of providing a special API call for anonymous authentication,
Smack now has a configuration builder method to set anonymous/external
authentication. This also removes a lot of duplicate code within
Smack.

Also move SASLAnonymous into o.j.smack.sasl.core.

Fixes SMACK-629.
This commit is contained in:
Florian Schmaus 2015-05-03 22:00:41 +02:00
parent 51700400bc
commit 9354e4fb74
10 changed files with 240 additions and 214 deletions

View file

@ -210,42 +210,16 @@ public class XMPPBOSHConnection extends AbstractXMPPConnection {
}
@Override
protected void loginNonAnonymously(String username, String password, String resource)
throws XMPPException, SmackException, IOException, InterruptedException {
if (saslAuthentication.hasNonAnonymousAuthentication()) {
// Authenticate using SASL
if (password != null) {
saslAuthentication.authenticate(username, password, resource);
} else {
saslAuthentication.authenticate(resource, config.getCallbackHandler());
}
} else {
throw new SmackException("No non-anonymous SASL authentication mechanism available");
}
protected void loginInternal(String username, String password, String resource) throws XMPPException,
SmackException, IOException, InterruptedException {
// Authenticate using SASL
saslAuthentication.authenticate(username, password);
bindResourceAndEstablishSession(resource);
afterSuccessfulLogin(false);
}
@Override
protected void loginAnonymously() throws XMPPException, SmackException, IOException, InterruptedException {
// Wait with SASL auth until the SASL mechanisms have been received
saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
if (saslAuthentication.hasAnonymousAuthentication()) {
saslAuthentication.authenticateAnonymously();
}
else {
// Authenticate using Non-SASL
throw new SmackException("No anonymous SASL authentication mechanism available");
}
bindResourceAndEstablishSession(null);
afterSuccessfulLogin(false);
}
@Override
public void send(PlainStreamElement element) throws NotConnectedException {
if (done) {

View file

@ -204,7 +204,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
/**
* The SASLAuthentication manager that is responsible for authenticating with the server.
*/
protected SASLAuthentication saslAuthentication = new SASLAuthentication(this);
protected final SASLAuthentication saslAuthentication;
/**
* A number to uniquely identify connections that are created. This is distinct from the
@ -291,6 +291,7 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
* @param configuration The configuration which is used to establish the connection.
*/
protected AbstractXMPPConnection(ConnectionConfiguration configuration) {
saslAuthentication = new SASLAuthentication(this, configuration);
config = configuration;
// Notify listeners that a new connection has been established
for (ConnectionCreationListener listener : XMPPConnectionRegistry.getConnectionCreationListeners()) {
@ -397,18 +398,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
* @throws InterruptedException
*/
public synchronized void login() throws XMPPException, SmackException, IOException, InterruptedException {
if (isAnonymous()) {
throwNotConnectedExceptionIfAppropriate();
throwAlreadyLoggedInExceptionIfAppropriate();
loginAnonymously();
} else {
// The previously used username, password and resource take over precedence over the
// ones from the connection configuration
CharSequence username = usedUsername != null ? usedUsername : config.getUsername();
String password = usedPassword != null ? usedPassword : config.getPassword();
String resource = usedResource != null ? usedResource : config.getResource();
login(username, password, resource);
}
// The previously used username, password and resource take over precedence over the
// ones from the connection configuration
CharSequence username = usedUsername != null ? usedUsername : config.getUsername();
String password = usedPassword != null ? usedPassword : config.getPassword();
String resource = usedResource != null ? usedResource : config.getResource();
login(username, password, resource);
}
/**
@ -451,14 +446,12 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
usedUsername = username != null ? username.toString() : null;
usedPassword = password;
usedResource = resource;
loginNonAnonymously(usedUsername, usedPassword, usedResource);
loginInternal(usedUsername, usedPassword, usedResource);
}
protected abstract void loginNonAnonymously(String username, String password, String resource)
protected abstract void loginInternal(String username, String password, String resource)
throws XMPPException, SmackException, IOException, InterruptedException;
protected abstract void loginAnonymously() throws XMPPException, SmackException, IOException, InterruptedException;
@Override
public final boolean isConnected() {
return connected;

View file

@ -17,8 +17,19 @@
package org.jivesoftware.smack;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.jivesoftware.smack.packet.Session;
import org.jivesoftware.smack.proxy.ProxyInfo;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.core.SASLAnonymous;
import org.jivesoftware.smack.util.CollectionUtil;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.StringUtils;
import org.jxmpp.jid.DomainBareJid;
import javax.net.SocketFactory;
@ -93,6 +104,8 @@ public abstract class ConnectionConfiguration {
protected final boolean allowNullOrEmptyUsername;
private final Set<String> enabledSaslMechanisms;
protected ConnectionConfiguration(Builder<?,?> builder) {
username = builder.username;
password = builder.password;
@ -130,6 +143,10 @@ public abstract class ConnectionConfiguration {
legacySessionDisabled = builder.legacySessionDisabled;
debuggerEnabled = builder.debuggerEnabled;
allowNullOrEmptyUsername = builder.allowEmptyOrNullUsername;
enabledSaslMechanisms = builder.enabledSaslMechanisms;
// If the enabledSaslmechanisms are set, then they must not be empty
assert(enabledSaslMechanisms != null ? !enabledSaslMechanisms.isEmpty() : true);
}
/**
@ -350,6 +367,24 @@ public abstract class ConnectionConfiguration {
return false;
}
/**
* Check if the given SASL mechansism is enabled in this connection configuration.
*
* @param saslMechanism
* @return true if the given SASL mechanism is enabled, false otherwise.
*/
public boolean isEnabledSaslMechanism(String saslMechanism) {
// If enabledSaslMechanisms is not set, then all mechanisms are enabled per default
if (enabledSaslMechanisms == null) {
return true;
}
return enabledSaslMechanisms.contains(saslMechanism);
}
public Set<String> getEnabledSaslMechanisms() {
return Collections.unmodifiableSet(enabledSaslMechanisms);
}
/**
* A builder for XMPP connection configurations.
* <p>
@ -387,6 +422,8 @@ public abstract class ConnectionConfiguration {
private String host;
private int port = 5222;
private boolean allowEmptyOrNullUsername = false;
private boolean saslMechanismsSealed;
private Set<String> enabledSaslMechanisms;
protected Builder() {
}
@ -518,7 +555,7 @@ public abstract class ConnectionConfiguration {
* @return a reference to this builder.
*/
public B setCustomSSLContext(SSLContext context) {
this.customSSLContext = context;
this.customSSLContext = Objects.requireNonNull(context, "The SSLContext must not be null");
return getThis();
}
@ -628,6 +665,92 @@ public abstract class ConnectionConfiguration {
return getThis();
}
/**
* Perform anonymous authentication using SASL ANONYMOUS. Your XMPP service must support this authentication
* mechanism. This method also calls {@link #addEnabledSaslMechanism(String)} with "ANONYMOUS" as argument.
*
* @return a reference to this builder.
*/
public B performSaslAnonymousAuthentication() {
if (!SASLAuthentication.isSaslMechanismRegistered(SASLAnonymous.NAME)) {
throw new IllegalArgumentException("SASL " + SASLAnonymous.NAME + " is not registered");
}
throwIfEnabledSaslMechanismsSet();
allowEmptyOrNullUsernames();
addEnabledSaslMechanism(SASLAnonymous.NAME);
saslMechanismsSealed = true;
return getThis();
}
/**
* Perform authentication using SASL EXTERNAL. Your XMPP service must support this
* authentication mechanism. This method also calls {@link #addEnabledSaslMechanism(String)} with "EXTERNAL" as
* argument. It also calls {@link #allowEmptyOrNullUsernames()} and {@link #setSecurityMode(SecurityMode)} to
* {@link SecurityMode#required}.
*
* @return a reference to this builder.
*/
public B performSaslExternalAuthentication(SSLContext sslContext) {
if (!SASLAuthentication.isSaslMechanismRegistered(SASLMechanism.EXTERNAL)) {
throw new IllegalArgumentException("SASL " + SASLMechanism.EXTERNAL + " is not registered");
}
setCustomSSLContext(sslContext);
throwIfEnabledSaslMechanismsSet();
allowEmptyOrNullUsernames();
setSecurityMode(SecurityMode.required);
addEnabledSaslMechanism(SASLMechanism.EXTERNAL);
saslMechanismsSealed = true;
return getThis();
}
private void throwIfEnabledSaslMechanismsSet() {
if (enabledSaslMechanisms != null) {
throw new IllegalStateException("Enabled SASL mechanisms found");
}
}
/**
* Add the given mechanism to the enabled ones. See {@link #addEnabledSaslMechanism(Collection)} for a discussion about enabled SASL mechanisms.
*
* @param saslMechanism the name of the mechanism to enable.
* @return a reference to this builder.
*/
public B addEnabledSaslMechanism(String saslMechanism) {
return addEnabledSaslMechanism(Arrays.asList(StringUtils.requireNotNullOrEmpty(saslMechanism,
"saslMechanism must not be null or empty")));
}
/**
* Enable the given SASL mechanisms. If you never add a mechanism to the set of enabled ones, <b>all mechanisms
* known to Smack</b> will be enabled. Only explicitly enable particular SASL mechanisms if you want to limit
* the used mechanisms to the enabled ones.
*
* @param saslMechanisms a collection of names of mechanisms to enable.
* @return a reference to this builder.
*/
public B addEnabledSaslMechanism(Collection<String> saslMechanisms) {
if (saslMechanismsSealed) {
throw new IllegalStateException("The enabled SASL mechanisms are sealed, you can not add new ones");
}
CollectionUtil.requireNotEmpty(saslMechanisms, "saslMechanisms");
Set<String> blacklistedMechanisms = SASLAuthentication.getBlacklistedSASLMechanisms();
for (String mechanism : saslMechanisms) {
if (!SASLAuthentication.isSaslMechanismRegistered(mechanism)) {
throw new IllegalArgumentException("SASL " + mechanism + " is not avaiable. Consider registering it with Smack");
}
if (blacklistedMechanisms.contains(mechanism)) {
throw new IllegalArgumentException("SALS " + mechanism + " is blacklisted.");
}
}
if (enabledSaslMechanisms == null) {
enabledSaslMechanisms = new HashSet<>(saslMechanisms.size());
}
enabledSaslMechanisms.addAll(saslMechanisms);
return getThis();
}
public abstract C build();
protected abstract B getThis();

View file

@ -20,11 +20,11 @@ package org.jivesoftware.smack;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Mechanisms;
import org.jivesoftware.smack.sasl.SASLAnonymous;
import org.jivesoftware.smack.sasl.SASLErrorException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure;
import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success;
import org.jxmpp.jid.DomainBareJid;
import javax.security.auth.callback.CallbackHandler;
@ -91,6 +91,17 @@ public class SASLAuthentication {
return answer;
}
public static boolean isSaslMechanismRegistered(String saslMechanism) {
synchronized (REGISTERED_MECHANISMS) {
for (SASLMechanism mechanism : REGISTERED_MECHANISMS) {
if (mechanism.getName().equals(saslMechanism)) {
return true;
}
}
}
return false;
}
/**
* Unregister a SASLMechanism by it's full class name. For example
* "org.jivesoftware.smack.sasl.javax.SASLCramMD5Mechanism".
@ -131,6 +142,7 @@ public class SASLAuthentication {
}
private final AbstractXMPPConnection connection;
private final ConnectionConfiguration configuration;
private SASLMechanism currentMechanism = null;
/**
@ -143,68 +155,12 @@ public class SASLAuthentication {
*/
private Exception saslException;
SASLAuthentication(AbstractXMPPConnection connection) {
SASLAuthentication(AbstractXMPPConnection connection, ConnectionConfiguration configuration) {
this.configuration = configuration;
this.connection = connection;
this.init();
}
/**
* Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.
*
* @return true if the server offered ANONYMOUS SASL as a way to authenticate users.
*/
public boolean hasAnonymousAuthentication() {
return serverMechanisms().contains("ANONYMOUS");
}
/**
* Returns true if the server offered SASL authentication besides ANONYMOUS SASL.
*
* @return true if the server offered SASL authentication besides ANONYMOUS SASL.
*/
public boolean hasNonAnonymousAuthentication() {
return !serverMechanisms().isEmpty() && (serverMechanisms().size() != 1 || !hasAnonymousAuthentication());
}
/**
* Performs SASL authentication of the specified user. If SASL authentication was successful
* then resource binding and session establishment will be performed. This method will return
* the full JID provided by the server while binding a resource to the connection.<p>
*
* The server may assign a full JID with a username or resource different than the requested
* by this method.
*
* @param resource the desired resource.
* @param cbh the CallbackHandler used to get information from the user
* @throws IOException
* @throws XMPPErrorException
* @throws SASLErrorException
* @throws SmackException
* @throws InterruptedException
*/
public void authenticate(String resource, CallbackHandler cbh) throws IOException,
XMPPErrorException, SASLErrorException, SmackException, InterruptedException {
SASLMechanism selectedMechanism = selectMechanism();
if (selectedMechanism != null) {
currentMechanism = selectedMechanism;
synchronized (this) {
currentMechanism.authenticate(connection.getHost(), connection.getServiceName(), cbh);
// Wait until SASL negotiation finishes
wait(connection.getPacketReplyTimeout());
}
maybeThrowException();
if (!authenticationSuccessful) {
throw NoResponseException.newWith(connection);
}
}
else {
throw new SmackException(
"SASL Authentication failed. No known authentication mechanisims.");
}
}
/**
* Performs SASL authentication of the specified user. If SASL authentication was successful
* then resource binding and session establishment will be performed. This method will return
@ -215,70 +171,31 @@ public class SASLAuthentication {
*
* @param username the username that is authenticating with the server.
* @param password the password to send to the server.
* @param resource the desired resource.
* @throws XMPPErrorException
* @throws SASLErrorException
* @throws IOException
* @throws SmackException
* @throws InterruptedException
*/
public void authenticate(String username, String password, String resource)
public void authenticate(String username, String password)
throws XMPPErrorException, SASLErrorException, IOException,
SmackException, InterruptedException {
SASLMechanism selectedMechanism = selectMechanism();
if (selectedMechanism != null) {
currentMechanism = selectedMechanism;
currentMechanism = selectMechanism();
final CallbackHandler callbackHandler = configuration.getCallbackHandler();
final String host = connection.getHost();
final DomainBareJid xmppDomain = connection.getServiceName();
synchronized (this) {
currentMechanism.authenticate(username, connection.getHost(),
connection.getServiceName(), password);
// Wait until SASL negotiation finishes
wait(connection.getPacketReplyTimeout());
}
maybeThrowException();
if (!authenticationSuccessful) {
throw NoResponseException.newWith(connection);
}
}
else {
throw new SmackException(
"SASL Authentication failed. No known authentication mechanisims.");
}
}
/**
* Performs ANONYMOUS SASL authentication. If SASL authentication was successful
* then resource binding and session establishment will be performed. This method will return
* the full JID provided by the server while binding a resource to the connection.<p>
*
* The server will assign a full JID with a randomly generated resource and possibly with
* no username.
*
* @throws SASLErrorException
* @throws XMPPErrorException if an error occures while authenticating.
* @throws SmackException if there was no response from the server.
* @throws InterruptedException
*/
public void authenticateAnonymously() throws SASLErrorException,
SmackException, XMPPErrorException, InterruptedException {
currentMechanism = (new SASLAnonymous()).instanceForAuthentication(connection);
// Wait until SASL negotiation finishes
synchronized (this) {
currentMechanism.authenticate(null, null, null, "");
if (callbackHandler != null) {
currentMechanism.authenticate(host, xmppDomain, callbackHandler);
}
else {
currentMechanism.authenticate(username, host, xmppDomain, password);
}
// Wait until SASL negotiation finishes
wait(connection.getPacketReplyTimeout());
}
maybeThrowException();
if (!authenticationSuccessful) {
throw NoResponseException.newWith(connection);
}
}
private void maybeThrowException() throws SmackException, SASLErrorException {
if (saslException != null){
if (saslException instanceof SmackException) {
throw (SmackException) saslException;
@ -288,6 +205,10 @@ public class SASLAuthentication {
throw new IllegalStateException("Unexpected exception type" , saslException);
}
}
if (!authenticationSuccessful) {
throw NoResponseException.newWith(connection);
}
}
/**
@ -378,10 +299,12 @@ public class SASLAuthentication {
saslException = null;
}
private SASLMechanism selectMechanism() {
// Locate the SASLMechanism to use
SASLMechanism selectedMechanism = null;
private SASLMechanism selectMechanism() throws SmackException {
Iterator<SASLMechanism> it = REGISTERED_MECHANISMS.iterator();
final List<String> serverMechanisms = getServerMechanisms();
if (serverMechanisms.isEmpty()) {
LOGGER.warning("Server did not report any SASL mechanisms");
}
// Iterate in SASL Priority order over registered mechanisms
while (it.hasNext()) {
SASLMechanism mechanism = it.next();
@ -391,19 +314,31 @@ public class SASLAuthentication {
continue;
}
}
if (serverMechanisms().contains(mechanismName)) {
if (!configuration.isEnabledSaslMechanism(mechanismName)) {
continue;
}
if (serverMechanisms.contains(mechanismName)) {
// Create a new instance of the SASLMechanism for every authentication attempt.
selectedMechanism = mechanism.instanceForAuthentication(connection);
break;
return mechanism.instanceForAuthentication(connection);
}
}
return selectedMechanism;
synchronized (BLACKLISTED_MECHANISMS) {
// @formatter:off
throw new SmackException(
"No supported and enabled SASL Mechanism provided by server. " +
"Server announced mechanisms: " + serverMechanisms + ". " +
"Registerd SASL mechanisms with Smack: " + REGISTERED_MECHANISMS + ". " +
"Enabled SASL mechansisms for this connection: " + configuration.getEnabledSaslMechanisms() + ". " +
"Blacklisted SASL mechanisms: " + BLACKLISTED_MECHANISMS + '.'
);
// @formatter;on
}
}
private List<String> serverMechanisms() {
private List<String> getServerMechanisms() {
Mechanisms mechanisms = connection.getFeature(Mechanisms.ELEMENT, Mechanisms.NAMESPACE);
if (mechanisms == null) {
LOGGER.warning("Server did not report any SASL mechanisms");
return Collections.emptyList();
}
return mechanisms.getMechanisms();

View file

@ -32,6 +32,7 @@ import org.jivesoftware.smack.initializer.SmackInitializer;
import org.jivesoftware.smack.packet.Bind;
import org.jivesoftware.smack.provider.BindIQProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.sasl.core.SASLAnonymous;
import org.jivesoftware.smack.sasl.core.SASLXOauth2Mechanism;
import org.jivesoftware.smack.sasl.core.SCRAMSHA1Mechanism;
import org.jivesoftware.smack.util.FileUtils;
@ -137,6 +138,7 @@ public final class SmackInitialization {
SASLAuthentication.registerSASLMechanism(new SCRAMSHA1Mechanism());
SASLAuthentication.registerSASLMechanism(new SASLXOauth2Mechanism());
SASLAuthentication.registerSASLMechanism(new SASLAnonymous());
ProviderManager.addIQProvider(Bind.ELEMENT, Bind.NAMESPACE, new BindIQProvider());

View file

@ -171,6 +171,9 @@ public abstract class SASLMechanism implements Comparable<SASLMechanism> {
authenticate();
}
/**
* @throws SmackException
*/
protected void authenticateInternal() throws SmackException {
}
@ -248,6 +251,9 @@ public abstract class SASLMechanism implements Comparable<SASLMechanism> {
connection.send(responseStanza);
}
/**
* @throws SmackException
*/
protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
return null;
}

View file

@ -14,9 +14,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.sasl;
package org.jivesoftware.smack.sasl.core;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.sasl.SASLMechanism;
import javax.security.auth.callback.CallbackHandler;

View file

@ -0,0 +1,33 @@
/**
*
* Copyright 2015 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.util;
import java.util.Collection;
public class CollectionUtil {
public static <T> Collection<T> requireNotEmpty(Collection<T> collection, String collectionName) {
if (collection == null) {
throw new NullPointerException(collectionName + " must not be null.");
}
if (collection.isEmpty()) {
throw new IllegalArgumentException(collectionName + " must not be empty.");
}
return collection;
}
}

View file

@ -116,23 +116,12 @@ public class DummyConnection extends AbstractXMPPConnection {
}
@Override
protected void loginNonAnonymously(String username, String password, String resource)
protected void loginInternal(String username, String password, String resource)
throws XMPPException {
user = getUserJid();
authenticated = true;
}
@Override
public void loginAnonymously() throws XMPPException {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
if (isAuthenticated()) {
throw new IllegalStateException("Already logged in to server.");
}
authenticated = true;
}
@Override
public void send(PlainStreamElement element) {
queue.add(element);

View file

@ -361,18 +361,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
}
@Override
protected synchronized void loginNonAnonymously(String username, String password, String resource) throws XMPPException, SmackException, IOException, InterruptedException {
if (saslAuthentication.hasNonAnonymousAuthentication()) {
// Authenticate using SASL
if (password != null) {
saslAuthentication.authenticate(username, password, resource);
}
else {
saslAuthentication.authenticate(resource, config.getCallbackHandler());
}
} else {
throw new SmackException("No non-anonymous SASL authentication mechanism available");
}
protected synchronized void loginInternal(String username, String password, String resource) throws XMPPException,
SmackException, IOException, InterruptedException {
// Authenticate using SASL
saslAuthentication.authenticate(username, password);
// If compression is enabled then request the server to use stream compression. XEP-170
// recommends to perform stream compression before resource binding.
@ -430,28 +422,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection {
afterSuccessfulLogin(false);
}
@Override
public synchronized void loginAnonymously() throws XMPPException, SmackException, IOException, InterruptedException {
// Wait with SASL auth until the SASL mechanisms have been received
saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
if (saslAuthentication.hasAnonymousAuthentication()) {
saslAuthentication.authenticateAnonymously();
}
else {
throw new SmackException("No anonymous SASL authentication mechanism available");
}
// If compression is enabled then request the server to use stream compression
if (config.isCompressionEnabled()) {
useCompression();
}
bindResourceAndEstablishSession(null);
afterSuccessfulLogin(false);
}
@Override
public boolean isSecureConnection() {
return usingTLS;