001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.chat.sshd; 019 020import org.apache.sshd.common.file.nonefs.NoneFileSystemFactory; 021import org.apache.sshd.common.session.SessionHeartbeatController; 022import org.apache.sshd.server.ServerBuilder; 023import org.apache.sshd.server.SshServer; 024import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; 025import org.apache.sshd.server.shell.ShellFactory; 026import org.jline.builtins.ssh.ShellFactoryImpl; 027 028import java.io.IOException; 029import java.nio.file.Path; 030import java.time.Duration; 031 032public class EnolaSshServer implements AutoCloseable { 033 034 private final SshServer server; 035 036 public EnolaSshServer(int port, Path hostKeyStorePath) throws IOException { 037 this(port, hostKeyStorePath, new ShellFactoryImpl(ChatShell::new)); 038 } 039 040 public EnolaSshServer(int port, Path hostKeyStorePath, ShellFactory shellFactory) 041 throws IOException { 042 var builder = ServerBuilder.builder(); 043 044 // NOTA BENE: Good God, no local file system access for Enola Chat over SSH! 045 builder.fileSystemFactory(NoneFileSystemFactory.INSTANCE); 046 047 server = builder.build(); 048 if (port != 0) server.setPort(port); 049 050 // TODO SimpleGeneratorHostKeyProvider vs. BouncyCastleGeneratorHostKeyProvider ?! 051 // NOTA BENE: We use Ed25519 instead of the (default) ecdsa-sha2-nistp521 here; 052 // see also https://github.com/apache/mina-sshd/issues/747 for some related background. 053 hostKeyStorePath.getParent().toFile().mkdirs(); 054 var keyPairProvider = new SimpleGeneratorHostKeyProvider(hostKeyStorePath); 055 keyPairProvider.setAlgorithm("Ed25519"); 056 server.setKeyPairProvider(keyPairProvider); 057 058 // NOTA BENE: For Enola Chat, we just accept all public keys! 059 var pubKeyAuthenticator = new NonLoggingAcceptAllPublickeyAuthenticator(); 060 server.setPublickeyAuthenticator(pubKeyAuthenticator); 061 062 server.setShellFactory(shellFactory); 063 064 // https://github.com/apache/mina-sshd/blob/master/docs/server-setup.md#providing-server-side-heartbeat 065 server.setSessionHeartbeat( 066 SessionHeartbeatController.HeartbeatType.IGNORE, Duration.ofSeconds(7)); 067 068 server.start(); 069 } 070 071 public int port() { 072 return server.getPort(); 073 } 074 075 @Override 076 public void close() throws Exception { 077 // NB: TODO Is server.close(); or server.stop(); correct? 078 server.stop(true); 079 } 080}