]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java
Merge remote-tracking branch 'origin/unstable' into testing
[lgpl/argeo-commons.git] / org.argeo.cms.lib.sshd / src / org / argeo / cms / ssh / CmsSshServer.java
1 package org.argeo.cms.ssh;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.io.Writer;
6 import java.net.InetSocketAddress;
7 import java.nio.charset.StandardCharsets;
8 import java.nio.file.Files;
9 import java.nio.file.Path;
10 import java.nio.file.Paths;
11 import java.nio.file.StandardOpenOption;
12 import java.nio.file.attribute.PosixFilePermission;
13 import java.security.KeyPair;
14 import java.security.KeyStore;
15 import java.security.KeyStoreException;
16 import java.security.NoSuchAlgorithmException;
17 import java.security.NoSuchProviderException;
18 import java.security.PrivateKey;
19 import java.security.UnrecoverableKeyException;
20 import java.security.cert.CertificateException;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.Set;
24
25 import org.apache.sshd.common.config.keys.PublicKeyEntry;
26 import org.apache.sshd.common.forward.PortForwardingEventListener;
27 import org.apache.sshd.common.keyprovider.KeyPairProvider;
28 import org.apache.sshd.common.session.Session;
29 import org.apache.sshd.common.util.net.SshdSocketAddress;
30 import org.apache.sshd.scp.server.ScpCommandFactory;
31 import org.apache.sshd.server.SshServer;
32 import org.apache.sshd.server.auth.gss.GSSAuthenticator;
33 import org.apache.sshd.server.config.keys.AuthorizedKeysAuthenticator;
34 import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator;
35 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
36 import org.apache.sshd.server.jaas.JaasPasswordAuthenticator;
37 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
38 import org.apache.sshd.server.shell.InteractiveProcessShellFactory;
39 import org.apache.sshd.sftp.server.SftpSubsystemFactory;
40 import org.argeo.api.cms.CmsAuth;
41 import org.argeo.api.cms.CmsConstants;
42 import org.argeo.api.cms.CmsLog;
43 import org.argeo.api.cms.CmsSshd;
44 import org.argeo.api.cms.CmsState;
45 import org.argeo.cms.CmsDeployProperty;
46
47 public class CmsSshServer implements CmsSshd {
48 private final static CmsLog log = CmsLog.getLog(CmsSshServer.class);
49
50 private CmsState cmsState;
51 private SshServer sshd = null;
52
53 private int port;
54 private String host;
55
56 public void start() {
57 String portStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_PORT.getProperty());
58 if (portStr == null)
59 return; // ignore
60 port = Integer.parseInt(portStr);
61
62 host = cmsState.getDeployProperty(CmsDeployProperty.HOST.getProperty());
63
64 KeyPair nodeKeyPair = loadNodeKeyPair();
65
66 try {
67 // authorized keys
68 String authorizedKeysStr = cmsState.getDeployProperty(CmsDeployProperty.SSHD_AUTHORIZEDKEYS.getProperty());
69 Path authorizedKeysPath = authorizedKeysStr != null ? Paths.get(authorizedKeysStr)
70 : AuthorizedKeysAuthenticator.getDefaultAuthorizedKeysFile();
71 if (authorizedKeysStr != null && !Files.exists(authorizedKeysPath)) {
72 Files.createFile(authorizedKeysPath);
73 Set<PosixFilePermission> posixPermissions = new HashSet<>();
74 posixPermissions.add(PosixFilePermission.OWNER_READ);
75 posixPermissions.add(PosixFilePermission.OWNER_WRITE);
76 Files.setPosixFilePermissions(authorizedKeysPath, posixPermissions);
77
78 if (nodeKeyPair != null)
79 try {
80 String openSsshPublicKey = PublicKeyEntry.toString(nodeKeyPair.getPublic());
81 try (Writer writer = Files.newBufferedWriter(authorizedKeysPath, StandardCharsets.US_ASCII,
82 StandardOpenOption.APPEND)) {
83 writer.write(openSsshPublicKey);
84 }
85 } catch (IOException e) {
86 log.error("Cannot add node public key to SSH authorized keys", e);
87 }
88 }
89
90 // create server
91 sshd = SshServer.setUpDefaultServer();
92 sshd.setPort(port);
93 if (host != null)
94 sshd.setHost(host);
95
96 // host key
97 if (nodeKeyPair != null) {
98 sshd.setKeyPairProvider(KeyPairProvider.wrap(nodeKeyPair));
99 } else {
100 Path hostKeyPath = cmsState.getDataPath(DEFAULT_SSH_HOST_KEY_PATH);
101 if (hostKeyPath == null) // TODO deal with no data area?
102 throw new IllegalStateException("An SSH server key must be set");
103 sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyPath));
104 }
105
106 // tunnels
107 sshd.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
108 sshd.addPortForwardingEventListener(new PortForwardingEventListener() {
109
110 @Override
111 public void establishingExplicitTunnel(Session session, SshdSocketAddress local,
112 SshdSocketAddress remote, boolean localForwarding) throws IOException {
113 log.debug("Establishing tunnel " + local + ", " + remote);
114 }
115
116 @Override
117 public void establishedExplicitTunnel(Session session, SshdSocketAddress local,
118 SshdSocketAddress remote, boolean localForwarding, SshdSocketAddress boundAddress,
119 Throwable reason) throws IOException {
120 log.debug("Established tunnel " + local + ", " + remote + ", " + boundAddress);
121 }
122
123 @Override
124 public void establishingDynamicTunnel(Session session, SshdSocketAddress local) throws IOException {
125 log.debug("Establishing dynamic tunnel " + local);
126 }
127
128 @Override
129 public void establishedDynamicTunnel(Session session, SshdSocketAddress local,
130 SshdSocketAddress boundAddress, Throwable reason) throws IOException {
131 log.debug("Established dynamic tunnel " + local);
132 }
133
134 });
135
136 // Authentication
137 // FIXME use strict, set proper permissions, etc.
138 sshd.setPublickeyAuthenticator(
139 new DefaultAuthorizedKeysAuthenticator("user.name", authorizedKeysPath, true));
140 // sshd.setPublickeyAuthenticator(null);
141 // sshd.setKeyboardInteractiveAuthenticator(null);
142 JaasPasswordAuthenticator jaasPasswordAuthenticator = new JaasPasswordAuthenticator();
143 jaasPasswordAuthenticator.setDomain(CmsAuth.NODE.getLoginContextName());
144 sshd.setPasswordAuthenticator(jaasPasswordAuthenticator);
145
146 boolean gssApi = false;
147 if (gssApi) {
148 Path krb5keyTab = cmsState.getDataPath("private/krb5.keytab");
149 if (Files.exists(krb5keyTab)) {
150 // FIXME experimental
151 GSSAuthenticator gssAuthenticator = new GSSAuthenticator();
152 gssAuthenticator.setKeytabFile(krb5keyTab.toString());
153 gssAuthenticator.setServicePrincipalName("HTTP@" + host);
154 sshd.setGSSAuthenticator(gssAuthenticator);
155 }
156 }
157
158 // shell
159 // TODO make it configurable
160 sshd.setShellFactory(InteractiveProcessShellFactory.INSTANCE);
161 // String[] shellCommand = OS.LOCAL.getDefaultShellCommand();
162 // StringJoiner command = new StringJoiner(" ");
163 // for (String str : shellCommand) {
164 // command.add(str);
165 // }
166 // sshd.setShellFactory(new ProcessShellFactory(command.toString(), shellCommand));
167 sshd.setCommandFactory(new ScpCommandFactory());
168
169 // SFTP
170 sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
171
172 // start
173 sshd.start();
174
175 log.debug(() -> "CMS SSH server started on port " + port + (host != null ? " of host " + host : ""));
176 } catch (IOException e) {
177 throw new RuntimeException("Cannot start SSH server on port " + port, e);
178 }
179
180 }
181
182 public void stop() {
183 if (sshd == null)
184 return;
185 try {
186 sshd.stop();
187 log.debug(() -> "CMS SSH server stopped on port " + port + (host != null ? " of host " + host : ""));
188 } catch (IOException e) {
189 throw new RuntimeException("Cannot stop SSH server", e);
190 }
191
192 }
193
194 protected KeyPair loadNodeKeyPair() {
195 try {
196 char[] keyStorePassword = cmsState.getDeployProperty(CmsDeployProperty.SSL_PASSWORD.getProperty())
197 .toCharArray();
198 Path keyStorePath = Paths.get(cmsState.getDeployProperty(CmsDeployProperty.SSL_KEYSTORE.getProperty()));
199 String keyStoreType = cmsState.getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE.getProperty());
200
201 KeyStore store = KeyStore.getInstance(keyStoreType, "SunJSSE");
202 try (InputStream fis = Files.newInputStream(keyStorePath)) {
203 store.load(fis, keyStorePassword);
204 }
205 return new KeyPair(store.getCertificate(CmsConstants.NODE).getPublicKey(),
206 (PrivateKey) store.getKey(CmsConstants.NODE, keyStorePassword));
207 } catch (IOException | KeyStoreException | NoSuchProviderException | NoSuchAlgorithmException
208 | CertificateException | IllegalArgumentException | UnrecoverableKeyException e) {
209 if (log.isTraceEnabled())
210 log.error("Cannot add node public key to SSH authorized keys", e);
211 else
212 log.error("Cannot add node public key to SSH authorized keys: " + e.getMessage());
213 return null;
214 }
215
216 }
217
218 public void setCmsState(CmsState cmsState) {
219 this.cmsState = cmsState;
220 }
221
222 @Override
223 public InetSocketAddress getAddress() {
224 return new InetSocketAddress(host, port);
225 }
226
227 }