< prev index next >

src/java.base/share/classes/sun/security/ssl/MaxFragExtension.java

Print this page

        

*** 1,7 **** /* ! * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this --- 1,7 ---- /* ! * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this
*** 24,139 **** */ package sun.security.ssl; import java.io.IOException; import javax.net.ssl.SSLProtocolException; ! /* ! * [RFC6066] TLS specifies a fixed maximum plaintext fragment length of ! * 2^14 bytes. It may be desirable for constrained clients to negotiate ! * a smaller maximum fragment length due to memory limitations or bandwidth ! * limitations. ! * ! * In order to negotiate smaller maximum fragment lengths, clients MAY ! * include an extension of type "max_fragment_length" in the (extended) ! * client hello. The "extension_data" field of this extension SHALL ! * contain: ! * ! * enum{ ! * 2^9(1), 2^10(2), 2^11(3), 2^12(4), (255) ! * } MaxFragmentLength; ! * ! * whose value is the desired maximum fragment length. */ ! final class MaxFragmentLengthExtension extends HelloExtension { ! private static final int MAX_FRAGMENT_LENGTH_512 = 1; // 2^9 ! private static final int MAX_FRAGMENT_LENGTH_1024 = 2; // 2^10 ! private static final int MAX_FRAGMENT_LENGTH_2048 = 3; // 2^11 ! private static final int MAX_FRAGMENT_LENGTH_4096 = 4; // 2^12 ! final int maxFragmentLength; ! MaxFragmentLengthExtension(int fragmentSize) { ! super(ExtensionType.EXT_MAX_FRAGMENT_LENGTH); ! if (fragmentSize < 1024) { ! maxFragmentLength = MAX_FRAGMENT_LENGTH_512; } else if (fragmentSize < 2048) { ! maxFragmentLength = MAX_FRAGMENT_LENGTH_1024; } else if (fragmentSize < 4096) { ! maxFragmentLength = MAX_FRAGMENT_LENGTH_2048; } else { ! maxFragmentLength = MAX_FRAGMENT_LENGTH_4096; } } ! MaxFragmentLengthExtension(HandshakeInStream s, int len) ! throws IOException { ! super(ExtensionType.EXT_MAX_FRAGMENT_LENGTH); ! // check the extension length ! if (len != 1) { ! throw new SSLProtocolException("Invalid " + type + " extension"); } ! maxFragmentLength = s.getInt8(); ! if ((maxFragmentLength > 4) || (maxFragmentLength < 1)) { ! throw new SSLProtocolException("Invalid " + type + " extension"); } } ! // Length of the encoded extension, including the type and length fields @Override ! int length() { ! return 5; // 4: extension type and length fields ! // 1: MaxFragmentLength field } @Override ! void send(HandshakeOutStream s) throws IOException { ! s.putInt16(type.id); ! s.putInt16(1); ! s.putInt8(maxFragmentLength); } ! int getMaxFragLen() { ! switch (maxFragmentLength) { ! case MAX_FRAGMENT_LENGTH_512: ! return 512; ! case MAX_FRAGMENT_LENGTH_1024: ! return 1024; ! case MAX_FRAGMENT_LENGTH_2048: ! return 2048; ! case MAX_FRAGMENT_LENGTH_4096: ! return 4096; } ! // unlikely to happen ! return -1; } ! static boolean needFragLenNego(int fragmentSize) { ! return (fragmentSize > 0) && (fragmentSize <= 4096); } ! static int getValidMaxFragLen(int fragmentSize) { ! if (fragmentSize < 1024) { ! return 512; ! } else if (fragmentSize < 2048) { ! return 1024; ! } else if (fragmentSize < 4096) { ! return 2048; ! } else if (fragmentSize == 4096) { ! return 4096; ! } else { ! return 16384; } } @Override ! public String toString() { ! return "Extension " + type + ", max_fragment_length: " + ! "(2^" + (maxFragmentLength + 8) + ")"; } } --- 24,620 ---- */ package sun.security.ssl; import java.io.IOException; + import java.nio.ByteBuffer; import javax.net.ssl.SSLProtocolException; + import static sun.security.ssl.SSLExtension.CH_MAX_FRAGMENT_LENGTH; + import static sun.security.ssl.SSLExtension.EE_MAX_FRAGMENT_LENGTH; + import sun.security.ssl.SSLExtension.ExtensionConsumer; + import static sun.security.ssl.SSLExtension.SH_MAX_FRAGMENT_LENGTH; + import sun.security.ssl.SSLExtension.SSLExtensionSpec; + import sun.security.ssl.SSLHandshake.HandshakeMessage; ! /** ! * Pack of the "max_fragment_length" extensions [RFC6066]. */ ! final class MaxFragExtension { ! static final HandshakeProducer chNetworkProducer = ! new CHMaxFragmentLengthProducer(); ! static final ExtensionConsumer chOnLoadConcumer = ! new CHMaxFragmentLengthConsumer(); ! ! static final HandshakeProducer shNetworkProducer = ! new SHMaxFragmentLengthProducer(); ! static final ExtensionConsumer shOnLoadConcumer = ! new SHMaxFragmentLengthConsumer(); ! static final HandshakeConsumer shOnTradeConsumer = ! new SHMaxFragmentLengthUpdate(); ! ! static final HandshakeProducer eeNetworkProducer = ! new EEMaxFragmentLengthProducer(); ! static final ExtensionConsumer eeOnLoadConcumer = ! new EEMaxFragmentLengthConsumer(); ! static final HandshakeConsumer eeOnTradeConsumer = ! new EEMaxFragmentLengthUpdate(); ! ! static final SSLStringize maxFragLenStringize = ! new MaxFragLenStringize(); ! ! /** ! * The "max_fragment_length" extension [RFC 6066]. ! */ ! static final class MaxFragLenSpec implements SSLExtensionSpec { ! byte id; ! ! private MaxFragLenSpec(byte id) { ! this.id = id; ! } ! ! private MaxFragLenSpec(ByteBuffer buffer) throws IOException { ! if (buffer.remaining() != 1) { ! throw new SSLProtocolException( ! "Invalid max_fragment_length extension data"); ! } ! ! this.id = buffer.get(); ! } ! ! @Override ! public String toString() { ! return MaxFragLenEnum.nameOf(id); ! } ! } ! ! private static final class MaxFragLenStringize implements SSLStringize { ! @Override ! public String toString(ByteBuffer buffer) { ! try { ! return (new MaxFragLenSpec(buffer)).toString(); ! } catch (IOException ioe) { ! // For debug logging only, so please swallow exceptions. ! return ioe.getMessage(); ! } ! } ! } ! ! static enum MaxFragLenEnum { ! MFL_512 ((byte)0x01, 512, "2^9"), ! MFL_1024 ((byte)0x02, 1024, "2^10"), ! MFL_2048 ((byte)0x03, 2048, "2^11"), ! MFL_4096 ((byte)0x04, 4096, "2^12"); ! final byte id; ! final int fragmentSize; ! final String description; ! private MaxFragLenEnum(byte id, int fragmentSize, String description) { ! this.id = id; ! this.fragmentSize = fragmentSize; ! this.description = description; ! } ! ! private static MaxFragLenEnum valueOf(byte id) { ! for (MaxFragLenEnum mfl : MaxFragLenEnum.values()) { ! if (mfl.id == id) { ! return mfl; ! } ! } ! ! return null; ! } ! ! private static String nameOf(byte id) { ! for (MaxFragLenEnum mfl : MaxFragLenEnum.values()) { ! if (mfl.id == id) { ! return mfl.description; ! } ! } ! return "UNDEFINED-MAX-FRAGMENT-LENGTH(" + id + ")"; ! } ! /** ! * Returns the best match enum constant of the specified ! * fragment size. ! */ ! static MaxFragLenEnum valueOf(int fragmentSize) { ! if (fragmentSize <= 0) { ! return null; ! } else if (fragmentSize < 1024) { ! return MFL_512; } else if (fragmentSize < 2048) { ! return MFL_1024; } else if (fragmentSize < 4096) { ! return MFL_2048; ! } else if (fragmentSize == 4096) { ! return MFL_4096; ! } ! ! return null; ! } ! } ! ! /** ! * Network data producer of a "max_fragment_length" extension in ! * the ClientHello handshake message. ! */ ! private static final ! class CHMaxFragmentLengthProducer implements HandshakeProducer { ! // Prevent instantiation of this class. ! private CHMaxFragmentLengthProducer() { ! // blank ! } ! ! @Override ! public byte[] produce(ConnectionContext context, ! HandshakeMessage message) throws IOException { ! // The producing happens in client side only. ! ClientHandshakeContext chc = (ClientHandshakeContext)context; ! ! // Is it a supported and enabled extension? ! if (!chc.sslConfig.isAvailable(CH_MAX_FRAGMENT_LENGTH)) { ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "Ignore unavailable max_fragment_length extension"); ! } ! return null; ! } ! ! // Produce the extension and update the context. ! int requestedMFLength; ! if (chc.isResumption && (chc.resumingSession != null)) { ! // The same extension should be sent for resumption. ! requestedMFLength = ! chc.resumingSession.getNegotiatedMaxFragSize(); ! } else if (chc.sslConfig.maximumPacketSize != 0) { ! // Maybe we can calculate the fragment size more accurate ! // by condering the enabled cipher suites in the future. ! requestedMFLength = chc.sslConfig.maximumPacketSize; ! if (chc.sslContext.isDTLS()) { ! requestedMFLength -= DTLSRecord.maxPlaintextPlusSize; } else { ! requestedMFLength -= SSLRecord.maxPlaintextPlusSize; } + } else { + // Need no max_fragment_length extension. + requestedMFLength = -1; } ! MaxFragLenEnum mfl = MaxFragLenEnum.valueOf(requestedMFLength); ! if (mfl != null) { ! // update the context. ! chc.handshakeExtensions.put( ! CH_MAX_FRAGMENT_LENGTH, new MaxFragLenSpec(mfl.id)); ! return new byte[] { mfl.id }; ! } else { ! // log and ignore, no MFL extension. ! chc.maxFragmentLength = -1; ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "No available max_fragment_length extension can " + ! "be used for fragment size of " + ! requestedMFLength + "bytes"); ! } } ! return null; } } ! /** ! * Network data consumer of a "max_fragment_length" extension in ! * the ClientHello handshake message. ! */ ! private static final ! class CHMaxFragmentLengthConsumer implements ExtensionConsumer { ! // Prevent instantiation of this class. ! private CHMaxFragmentLengthConsumer() { ! // blank ! } ! @Override ! public void consume(ConnectionContext context, ! HandshakeMessage message, ByteBuffer buffer) throws IOException { ! // The comsuming happens in server side only. ! ServerHandshakeContext shc = (ServerHandshakeContext)context; ! ! if (!shc.sslConfig.isAvailable(CH_MAX_FRAGMENT_LENGTH)) { ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "Ignore unavailable max_fragment_length extension"); ! } ! return; // ignore the extension ! } ! ! // Parse the extension. ! MaxFragLenSpec spec; ! try { ! spec = new MaxFragLenSpec(buffer); ! } catch (IOException ioe) { ! shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe); ! return; // fatal() always throws, make the compiler happy. ! } ! ! MaxFragLenEnum mfle = MaxFragLenEnum.valueOf(spec.id); ! if (mfle == null) { ! shc.conContext.fatal(Alert.ILLEGAL_PARAMETER, ! "the requested maximum fragment length is other " + ! "than the allowed values"); ! } ! ! // Update the context. ! shc.maxFragmentLength = mfle.fragmentSize; ! shc.handshakeExtensions.put(CH_MAX_FRAGMENT_LENGTH, spec); ! ! // No impact on session resumption. ! } ! } ! ! /** ! * Network data producer of a "max_fragment_length" extension in ! * the ServerHello handshake message. ! */ ! private static final ! class SHMaxFragmentLengthProducer implements HandshakeProducer { ! // Prevent instantiation of this class. ! private SHMaxFragmentLengthProducer() { ! // blank } @Override ! public byte[] produce(ConnectionContext context, ! HandshakeMessage message) throws IOException { ! // The producing happens in server side only. ! ServerHandshakeContext shc = (ServerHandshakeContext)context; ! ! // In response to "max_fragment_length" extension request only ! MaxFragLenSpec spec = (MaxFragLenSpec) ! shc.handshakeExtensions.get(CH_MAX_FRAGMENT_LENGTH); ! if (spec == null) { ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.finest( ! "Ignore unavailable max_fragment_length extension"); ! } ! return null; // ignore the extension ! } ! ! if ((shc.maxFragmentLength > 0) && ! (shc.sslConfig.maximumPacketSize != 0)) { ! int estimatedMaxFragSize = ! shc.negotiatedCipherSuite.calculatePacketSize( ! shc.maxFragmentLength, shc.negotiatedProtocol, ! shc.sslContext.isDTLS()); ! if (estimatedMaxFragSize > shc.sslConfig.maximumPacketSize) { ! // For better interoperability, abort the maximum ! // fragment length negotiation, rather than terminate ! // the connection with a fatal alert. ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "Abort the maximum fragment length negotiation, " + ! "may overflow the maximum packet size limit."); ! } ! shc.maxFragmentLength = -1; ! } } ! // update the context ! if (shc.maxFragmentLength > 0) { ! shc.handshakeSession.setNegotiatedMaxFragSize( ! shc.maxFragmentLength); ! shc.conContext.inputRecord.changeFragmentSize( ! shc.maxFragmentLength); ! shc.conContext.outputRecord.changeFragmentSize( ! shc.maxFragmentLength); ! ! // The response extension data is the same as the requested one. ! shc.handshakeExtensions.put(SH_MAX_FRAGMENT_LENGTH, spec); ! return new byte[] { spec.id }; ! } ! ! return null; ! } } ! /** ! * Network data consumer of a "max_fragment_length" extension in ! * the ServerHello handshake message. ! */ ! private static final ! class SHMaxFragmentLengthConsumer implements ExtensionConsumer { ! // Prevent instantiation of this class. ! private SHMaxFragmentLengthConsumer() { ! // blank } ! @Override ! public void consume(ConnectionContext context, ! HandshakeMessage message, ByteBuffer buffer) throws IOException { ! ! // The comsuming happens in client side only. ! ClientHandshakeContext chc = (ClientHandshakeContext)context; ! ! // In response to "max_fragment_length" extension request only ! MaxFragLenSpec requestedSpec = (MaxFragLenSpec) ! chc.handshakeExtensions.get(CH_MAX_FRAGMENT_LENGTH); ! if (requestedSpec == null) { ! chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ! "Unexpected max_fragment_length extension in ServerHello"); } ! // Parse the extension. ! MaxFragLenSpec spec; ! try { ! spec = new MaxFragLenSpec(buffer); ! } catch (IOException ioe) { ! chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe); ! return; // fatal() always throws, make the compiler happy. ! } ! ! if (spec.id != requestedSpec.id) { ! chc.conContext.fatal(Alert.ILLEGAL_PARAMETER, ! "The maximum fragment length response is not requested"); ! } ! ! MaxFragLenEnum mfle = MaxFragLenEnum.valueOf(spec.id); ! if (mfle == null) { ! chc.conContext.fatal(Alert.ILLEGAL_PARAMETER, ! "the requested maximum fragment length is other " + ! "than the allowed values"); ! } ! ! // update the context ! chc.maxFragmentLength = mfle.fragmentSize; ! chc.handshakeExtensions.put(SH_MAX_FRAGMENT_LENGTH, spec); } } + /** + * After session creation consuming of a "max_fragment_length" + * extension in the ClientHello handshake message. + */ + private static final class SHMaxFragmentLengthUpdate + implements HandshakeConsumer { + + // Prevent instantiation of this class. + private SHMaxFragmentLengthUpdate() { + // blank + } + @Override ! public void consume(ConnectionContext context, ! HandshakeMessage message) throws IOException { ! // The comsuming happens in client side only. ! ClientHandshakeContext chc = (ClientHandshakeContext)context; ! ! MaxFragLenSpec spec = (MaxFragLenSpec) ! chc.handshakeExtensions.get(SH_MAX_FRAGMENT_LENGTH); ! if (spec == null) { ! // Ignore, no "max_fragment_length" extension response. ! return; ! } ! ! if ((chc.maxFragmentLength > 0) && ! (chc.sslConfig.maximumPacketSize != 0)) { ! int estimatedMaxFragSize = ! chc.negotiatedCipherSuite.calculatePacketSize( ! chc.maxFragmentLength, chc.negotiatedProtocol, ! chc.sslContext.isDTLS()); ! if (estimatedMaxFragSize > chc.sslConfig.maximumPacketSize) { ! // For better interoperability, abort the maximum ! // fragment length negotiation, rather than terminate ! // the connection with a fatal alert. ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "Abort the maximum fragment length negotiation, " + ! "may overflow the maximum packet size limit."); ! } ! chc.maxFragmentLength = -1; ! } ! } ! ! // update the context ! if (chc.maxFragmentLength > 0) { ! chc.handshakeSession.setNegotiatedMaxFragSize( ! chc.maxFragmentLength); ! chc.conContext.inputRecord.changeFragmentSize( ! chc.maxFragmentLength); ! chc.conContext.outputRecord.changeFragmentSize( ! chc.maxFragmentLength); ! } ! } ! } ! ! /** ! * Network data producer of a "max_fragment_length" extension in ! * the EncryptedExtensions handshake message. ! */ ! private static final ! class EEMaxFragmentLengthProducer implements HandshakeProducer { ! // Prevent instantiation of this class. ! private EEMaxFragmentLengthProducer() { ! // blank ! } ! ! @Override ! public byte[] produce(ConnectionContext context, ! HandshakeMessage message) throws IOException { ! // The producing happens in server side only. ! ServerHandshakeContext shc = (ServerHandshakeContext)context; ! ! // In response to "max_fragment_length" extension request only ! MaxFragLenSpec spec = (MaxFragLenSpec) ! shc.handshakeExtensions.get(CH_MAX_FRAGMENT_LENGTH); ! if (spec == null) { ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.finest( ! "Ignore unavailable max_fragment_length extension"); ! } ! return null; // ignore the extension ! } ! ! if ((shc.maxFragmentLength > 0) && ! (shc.sslConfig.maximumPacketSize != 0)) { ! int estimatedMaxFragSize = ! shc.negotiatedCipherSuite.calculatePacketSize( ! shc.maxFragmentLength, shc.negotiatedProtocol, ! shc.sslContext.isDTLS()); ! if (estimatedMaxFragSize > shc.sslConfig.maximumPacketSize) { ! // For better interoperability, abort the maximum ! // fragment length negotiation, rather than terminate ! // the connection with a fatal alert. ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "Abort the maximum fragment length negotiation, " + ! "may overflow the maximum packet size limit."); ! } ! shc.maxFragmentLength = -1; ! } ! } ! ! // update the context ! if (shc.maxFragmentLength > 0) { ! shc.handshakeSession.setNegotiatedMaxFragSize( ! shc.maxFragmentLength); ! shc.conContext.inputRecord.changeFragmentSize( ! shc.maxFragmentLength); ! shc.conContext.outputRecord.changeFragmentSize( ! shc.maxFragmentLength); ! ! // The response extension data is the same as the requested one. ! shc.handshakeExtensions.put(EE_MAX_FRAGMENT_LENGTH, spec); ! return new byte[] { spec.id }; ! } ! ! return null; ! } ! } ! ! /** ! * Network data consumer of a "max_fragment_length" extension in the ! * EncryptedExtensions handshake message. ! */ ! private static final ! class EEMaxFragmentLengthConsumer implements ExtensionConsumer { ! // Prevent instantiation of this class. ! private EEMaxFragmentLengthConsumer() { ! // blank ! } ! ! @Override ! public void consume(ConnectionContext context, ! HandshakeMessage message, ByteBuffer buffer) throws IOException { ! // The comsuming happens in client side only. ! ClientHandshakeContext chc = (ClientHandshakeContext)context; ! ! // In response to "max_fragment_length" extension request only ! MaxFragLenSpec requestedSpec = (MaxFragLenSpec) ! chc.handshakeExtensions.get(CH_MAX_FRAGMENT_LENGTH); ! if (requestedSpec == null) { ! chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ! "Unexpected max_fragment_length extension in ServerHello"); ! } ! ! // Parse the extension. ! MaxFragLenSpec spec; ! try { ! spec = new MaxFragLenSpec(buffer); ! } catch (IOException ioe) { ! chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, ioe); ! return; // fatal() always throws, make the compiler happy. ! } ! ! if (spec.id != requestedSpec.id) { ! chc.conContext.fatal(Alert.ILLEGAL_PARAMETER, ! "The maximum fragment length response is not requested"); ! } ! ! MaxFragLenEnum mfle = MaxFragLenEnum.valueOf(spec.id); ! if (mfle == null) { ! chc.conContext.fatal(Alert.ILLEGAL_PARAMETER, ! "the requested maximum fragment length is other " + ! "than the allowed values"); ! } ! ! // update the context ! chc.maxFragmentLength = mfle.fragmentSize; ! chc.handshakeExtensions.put(EE_MAX_FRAGMENT_LENGTH, spec); ! } ! } ! ! /** ! * After session creation consuming of a "max_fragment_length" ! * extension in the EncryptedExtensions handshake message. ! */ ! private static final ! class EEMaxFragmentLengthUpdate implements HandshakeConsumer { ! // Prevent instantiation of this class. ! private EEMaxFragmentLengthUpdate() { ! // blank ! } ! ! @Override ! public void consume(ConnectionContext context, ! HandshakeMessage message) throws IOException { ! // The comsuming happens in client side only. ! ClientHandshakeContext chc = (ClientHandshakeContext)context; ! ! MaxFragLenSpec spec = (MaxFragLenSpec) ! chc.handshakeExtensions.get(EE_MAX_FRAGMENT_LENGTH); ! if (spec == null) { ! // Ignore, no "max_fragment_length" extension response. ! return; ! } ! ! if ((chc.maxFragmentLength > 0) && ! (chc.sslConfig.maximumPacketSize != 0)) { ! int estimatedMaxFragSize = ! chc.negotiatedCipherSuite.calculatePacketSize( ! chc.maxFragmentLength, chc.negotiatedProtocol, ! chc.sslContext.isDTLS()); ! if (estimatedMaxFragSize > chc.sslConfig.maximumPacketSize) { ! // For better interoperability, abort the maximum ! // fragment length negotiation, rather than terminate ! // the connection with a fatal alert. ! if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { ! SSLLogger.fine( ! "Abort the maximum fragment length negotiation, " + ! "may overflow the maximum packet size limit."); ! } ! chc.maxFragmentLength = -1; ! } ! } ! ! // update the context ! if (chc.maxFragmentLength > 0) { ! chc.handshakeSession.setNegotiatedMaxFragSize( ! chc.maxFragmentLength); ! chc.conContext.inputRecord.changeFragmentSize( ! chc.maxFragmentLength); ! chc.conContext.outputRecord.changeFragmentSize( ! chc.maxFragmentLength); ! } ! } } }
< prev index next >