/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.sshd.common.session.helpers; import java.io.IOException; import java.net.SocketAddress; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.channel.IoWriteFutureImpl; import org.apache.sshd.common.future.CloseFuture; import org.apache.sshd.common.future.DefaultCloseFuture; import org.apache.sshd.common.future.SshFutureListener; import org.apache.sshd.common.io.IoService; import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.session.ReservedSessionMessagesHandler; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.buffer.Buffer; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; import org.apache.sshd.util.test.BaseTestSupport; import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; /** * Test basic stuff on AbstractSession. * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class AbstractSessionTest extends BaseTestSupport { private MySession session; public AbstractSessionTest() { super(); } @Before public void setUp() throws Exception { session = new MySession(); } public void tearDown() throws Exception { if (session != null) { session.close(); } } @Test public void testReadIdentSimple() { Buffer buf = new ByteArrayBuffer("SSH-2.0-software\r\n".getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); assertEquals("SSH-2.0-software", ident); } @Test public void testReadIdentWithoutCR() { Buffer buf = new ByteArrayBuffer("SSH-2.0-software\n".getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); assertEquals("SSH-2.0-software", ident); } @Test public void testReadIdentWithHeaders() { Buffer buf = new ByteArrayBuffer("a header line\r\nSSH-2.0-software\r\n".getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); assertEquals("SSH-2.0-software", ident); } @Test public void testReadIdentWithSplitPackets() { Buffer buf = new ByteArrayBuffer("header line\r\nSSH".getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); assertNull("Unexpected identification for header only", ident); buf.putRawBytes("-2.0-software\r\n".getBytes(StandardCharsets.UTF_8)); ident = readIdentification(session, buf); assertEquals("SSH-2.0-software", ident); } @Test(expected = IllegalStateException.class) public void testReadIdentBadLineEnding() { Buffer buf = new ByteArrayBuffer("SSH-2.0-software\ra".getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); fail("Unexpected success: " + ident); } @Test(expected = IllegalStateException.class) public void testReadIdentLongLine() { StringBuilder sb = new StringBuilder(Session.MAX_VERSION_LINE_LENGTH + Integer.SIZE); sb.append("SSH-2.0-software"); do { sb.append("01234567890123456789012345678901234567890123456789"); } while (sb.length() < Session.MAX_VERSION_LINE_LENGTH); Buffer buf = new ByteArrayBuffer(sb.toString().getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); fail("Unexpected success: " + ident); } @Test(expected = IllegalStateException.class) public void testReadIdentWithNullChar() { String id = "SSH-2.0" + '\0' + "-software\r\n"; Buffer buf = new ByteArrayBuffer(id.getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); fail("Unexpected success: " + ident); } @Test(expected = IllegalStateException.class) public void testReadIdentLongHeader() { StringBuilder sb = new StringBuilder(FactoryManager.DEFAULT_MAX_IDENTIFICATION_SIZE + Integer.SIZE); do { sb.append("01234567890123456789012345678901234567890123456789\r\n"); } while (sb.length() < FactoryManager.DEFAULT_MAX_IDENTIFICATION_SIZE); sb.append("SSH-2.0-software\r\n"); Buffer buf = new ByteArrayBuffer(sb.toString().getBytes(StandardCharsets.UTF_8)); String ident = readIdentification(session, buf); fail("Unexpected success: " + ident); } @Test // see SSHD-619 public void testMsgIgnorePadding() throws Exception { final long frequency = Byte.SIZE; PropertyResolverUtils.updateProperty(session, FactoryManager.IGNORE_MESSAGE_SIZE, Short.SIZE); PropertyResolverUtils.updateProperty(session, FactoryManager.IGNORE_MESSAGE_FREQUENCY, frequency); PropertyResolverUtils.updateProperty(session, FactoryManager.IGNORE_MESSAGE_VARIANCE, 0); session.refreshConfiguration(); Buffer msg = session.createBuffer(SshConstants.SSH_MSG_DEBUG, Long.SIZE); msg.putBoolean(true); // display ? msg.putString(getCurrentTestName()); // message msg.putString(""); // language MyIoSession ioSession = (MyIoSession) session.getIoSession(); Queue<Buffer> queue = ioSession.getOutgoingMessages(); int numIgnores = 0; for (int cycle = 0; cycle < Byte.SIZE; cycle++) { for (long index = 0; index <= frequency; index++) { session.writePacket(msg); Buffer data = queue.remove(); if (data != msg) { int cmd = data.getUByte(); assertEquals("Mismatched buffer command at cycle " + cycle + "[" + index + "]", SshConstants.SSH_MSG_IGNORE, cmd); int len = data.getInt(); assertTrue("Mismatched random padding data length at cycle " + cycle + "[" + index + "]: " + len, len >= Short.SIZE); numIgnores++; } } } assertEquals("Mismatched number of ignore messages", Byte.SIZE, numIgnores); } @Test // see SSHD-652 public void testCloseFutureListenerRegistration() throws Exception { final AtomicInteger closeCount = new AtomicInteger(); session.addCloseFutureListener(future -> { assertTrue("Future not marted as closed", future.isClosed()); assertEquals("Unexpected multiple call to callback", 1, closeCount.incrementAndGet()); }); session.close(); assertEquals("Close listener not called", 1, closeCount.get()); } @Test // see SSHD-699 public void testMalformedUnimplementedMessage() throws Exception { session.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() { @Override public void handleUnimplementedMessage(Session session, Buffer buffer) throws Exception { fail("Unexpected invocation: available=" + buffer.available()); } }); Buffer buffer = new ByteArrayBuffer(Long.SIZE); for (int index = 0; index < (Integer.BYTES - 1); index++) { buffer.putByte((byte) index); session.handleUnimplemented(buffer); } } @Test // see SSHD-699 public void testMalformedIgnoreMessageBadLength() throws Exception { session.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() { @Override public void handleIgnoreMessage(Session session, Buffer buffer) throws Exception { fail("Unexpected invocation: available=" + buffer.available()); } }); Buffer buffer = new ByteArrayBuffer(Long.SIZE); for (int index = 0; index < (Integer.BYTES - 1); index++) { buffer.putByte((byte) index); session.handleIgnore(buffer); } } @Test // see SSHD-699 public void testMalformedIgnoreMessageCorruptedData() throws Exception { session.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() { @Override public void handleIgnoreMessage(Session session, Buffer buffer) throws Exception { fail("Unexpected invocation: available=" + buffer.available()); } }); Buffer buffer = new ByteArrayBuffer(Long.SIZE + Byte.MAX_VALUE); buffer.putInt(Byte.MAX_VALUE + 1); // bad message length for (int index = 0; index < Byte.MAX_VALUE; index++) { buffer.putByte((byte) index); session.handleIgnore(buffer); } } @Test public void testMalformedDebugMessageContent() throws Exception { session.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() { @Override public void handleDebugMessage(Session session, Buffer buffer) throws Exception { fail("Unexpected invocation: available=" + buffer.available()); } }); Buffer buffer = new ByteArrayBuffer(Long.SIZE + Byte.MAX_VALUE); session.handleDebug(buffer); // no boolean field buffer.putBoolean(true); session.handleDebug(buffer); // no message field buffer.putInt(Byte.MAX_VALUE + 1); // bad message field length for (int index = 0; index < Byte.MAX_VALUE; index++) { buffer.putByte((byte) index); session.handleDebug(buffer); } } @Test public void testMalformedDebugMessageLanguage() throws Exception { session.setReservedSessionMessagesHandler(new ReservedSessionMessagesHandler() { @Override public void handleDebugMessage(Session session, Buffer buffer) throws Exception { fail("Unexpected invocation: available=" + buffer.available()); } }); Buffer buffer = new ByteArrayBuffer(Long.SIZE); buffer.putBoolean(true); buffer.putString(getCurrentTestName()); session.handleDebug(buffer); // no language tag buffer.putInt(Byte.SIZE + 1); // bad language tag length for (int index = 0; index < Byte.SIZE; index++) { buffer.putByte((byte) index); session.handleDebug(buffer); } } private static String readIdentification(MySession session, Buffer buf) { List<String> lines = session.doReadIdentification(buf); return GenericUtils.isEmpty(lines) ? null : lines.get(lines.size() - 1); } public static class MyIoSession implements IoSession { private final Queue<Buffer> outgoing = new LinkedBlockingQueue<>(); private final AtomicBoolean open = new AtomicBoolean(true); private final CloseFuture closeFuture; public MyIoSession() { closeFuture = new DefaultCloseFuture(open); } @Override public void addCloseFutureListener(SshFutureListener<CloseFuture> listener) { closeFuture.addListener(listener); } @Override public void removeCloseFutureListener(SshFutureListener<CloseFuture> listener) { closeFuture.addListener(listener); } public Queue<Buffer> getOutgoingMessages() { return outgoing; } @Override public boolean isClosed() { return !isOpen(); } @Override public boolean isClosing() { return !isOpen(); } @Override public boolean isOpen() { return open.get(); } @Override public void close() throws IOException { close(true); } @Override public long getId() { return 0; } @Override public Object getAttribute(Object key) { return null; } @Override public Object setAttribute(Object key, Object value) { return null; } @Override public SocketAddress getRemoteAddress() { return null; } @Override public SocketAddress getLocalAddress() { return null; } @Override public IoWriteFuture write(Buffer buffer) { if (!isOpen()) { throw new IllegalStateException("Not open"); } if (!outgoing.offer(buffer)) { throw new IllegalStateException("Failed to offer outgoing buffer"); } IoWriteFutureImpl future = new IoWriteFutureImpl(buffer); future.setValue(Boolean.TRUE); return future; } @Override public CloseFuture close(boolean immediately) { if (open.getAndSet(false)) { outgoing.clear(); closeFuture.setClosed(); } return closeFuture; } @Override public IoService getService() { return null; } } public static class MySession extends AbstractSession { public MySession() { super(true, org.apache.sshd.util.test.Utils.setupTestServer(AbstractSessionTest.class), new MyIoSession()); } @Override protected void handleMessage(Buffer buffer) throws Exception { // ignored } @Override protected boolean readIdentification(Buffer buffer) { return false; } public List<String> doReadIdentification(Buffer buffer) { return super.doReadIdentification(buffer, false); } @Override protected Buffer encode(Buffer buffer) throws IOException { return buffer; } @Override protected byte[] sendKexInit() throws IOException { return GenericUtils.EMPTY_BYTE_ARRAY; } @Override protected void receiveKexInit(Map<KexProposalOption, String> proposal, byte[] seed) throws IOException { // ignored } @Override protected void setKexSeed(byte... seed) { // ignored } @Override protected String resolveAvailableSignaturesProposal(FactoryManager manager) { return null; } @Override protected void checkKeys() { // ignored } @Override public void startService(String name) throws Exception { // ignored } @Override public void resetIdleTimeout() { // ignored } } }