Line data Source code
1 : // Copyright (C) 2008 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.sshd; 16 : 17 : import com.google.gerrit.entities.Account; 18 : import com.google.gerrit.server.IdentifiedUser; 19 : import com.google.gerrit.server.config.AnonymousCowardName; 20 : import com.google.gerrit.server.config.CanonicalWebUrl; 21 : import com.google.gerrit.server.ssh.SshInfo; 22 : import com.google.gerrit.sshd.SshScope.Context; 23 : import com.google.inject.Inject; 24 : import com.google.inject.Provider; 25 : import java.io.IOException; 26 : import java.io.InputStream; 27 : import java.io.OutputStream; 28 : import java.net.MalformedURLException; 29 : import java.net.URL; 30 : import org.apache.sshd.common.io.IoInputStream; 31 : import org.apache.sshd.common.io.IoOutputStream; 32 : import org.apache.sshd.common.util.buffer.ByteArrayBuffer; 33 : import org.apache.sshd.server.Environment; 34 : import org.apache.sshd.server.ExitCallback; 35 : import org.apache.sshd.server.channel.ChannelSession; 36 : import org.apache.sshd.server.command.AsyncCommand; 37 : import org.apache.sshd.server.command.Command; 38 : import org.apache.sshd.server.session.ServerSession; 39 : import org.apache.sshd.server.session.ServerSessionAware; 40 : import org.apache.sshd.server.shell.ShellFactory; 41 : import org.eclipse.jgit.lib.Constants; 42 : import org.eclipse.jgit.util.SystemReader; 43 : 44 : /** 45 : * Dummy shell which prints a message and terminates. 46 : * 47 : * <p>This implementation is used to ensure clients who try to SSH directly to this server without 48 : * supplying a command will get a reasonable error message, but cannot continue further. 49 : */ 50 : class NoShell implements ShellFactory { 51 : private final Provider<SendMessage> shell; 52 : 53 : @Inject 54 17 : NoShell(Provider<SendMessage> shell) { 55 17 : this.shell = shell; 56 17 : } 57 : 58 : @Override 59 : public Command createShell(ChannelSession channel) { 60 1 : return shell.get(); 61 : } 62 : 63 : /** 64 : * When AsyncCommand is implemented by a command as below, the usual blocking streams aren't set. 65 : * 66 : * @see org.apache.sshd.server.command.AsyncCommand 67 : */ 68 : static class SendMessage implements AsyncCommand, ServerSessionAware { 69 : private final Provider<MessageFactory> messageFactory; 70 : private final SshScope sshScope; 71 : 72 : private IoInputStream in; 73 : private IoOutputStream out; 74 : private IoOutputStream err; 75 : 76 : private ExitCallback exit; 77 : private Context context; 78 : 79 : @Inject 80 1 : SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) { 81 1 : this.messageFactory = messageFactory; 82 1 : this.sshScope = sshScope; 83 1 : } 84 : 85 : @Override 86 : public void setIoInputStream(IoInputStream in) { 87 1 : this.in = in; 88 1 : } 89 : 90 : @Override 91 : public void setIoOutputStream(IoOutputStream out) { 92 1 : this.out = out; 93 1 : } 94 : 95 : @Override 96 : public void setIoErrorStream(IoOutputStream err) { 97 1 : this.err = err; 98 1 : } 99 : 100 : @Override 101 : public void setInputStream(InputStream in) { 102 : // ignored 103 0 : } 104 : 105 : @Override 106 : public void setOutputStream(OutputStream out) { 107 : // ignore 108 0 : } 109 : 110 : @Override 111 : public void setErrorStream(OutputStream err) { 112 : // ignore 113 0 : } 114 : 115 : @Override 116 : public void setExitCallback(ExitCallback callback) { 117 1 : this.exit = callback; 118 1 : } 119 : 120 : @Override 121 : public void setSession(ServerSession session) { 122 1 : SshSession s = session.getAttribute(SshSession.KEY); 123 1 : this.context = sshScope.newContext(s, ""); 124 1 : } 125 : 126 : @Override 127 : public void start(ChannelSession channel, Environment env) throws IOException { 128 1 : Context old = sshScope.set(context); 129 : String message; 130 : try { 131 1 : message = messageFactory.get().getMessage(); 132 : } finally { 133 1 : sshScope.set(old); 134 : } 135 1 : err.writeBuffer(new ByteArrayBuffer(Constants.encode(message))); 136 : 137 1 : in.close(); 138 1 : out.close(); 139 1 : err.close(); 140 1 : exit.onExit(127); 141 1 : } 142 : 143 : @Override 144 1 : public void destroy(ChannelSession channel) {} 145 : } 146 : 147 : static class MessageFactory { 148 : private final IdentifiedUser user; 149 : private final SshInfo sshInfo; 150 : private final Provider<String> urlProvider; 151 : private final String anonymousCowardName; 152 : 153 : @Inject 154 : MessageFactory( 155 : IdentifiedUser user, 156 : SshInfo sshInfo, 157 : @CanonicalWebUrl Provider<String> urlProvider, 158 1 : @AnonymousCowardName String anonymousCowardName) { 159 1 : this.user = user; 160 1 : this.sshInfo = sshInfo; 161 1 : this.urlProvider = urlProvider; 162 1 : this.anonymousCowardName = anonymousCowardName; 163 1 : } 164 : 165 : String getMessage() { 166 1 : StringBuilder msg = new StringBuilder(); 167 : 168 1 : msg.append("\r\n"); 169 1 : msg.append(" **** Welcome to Gerrit Code Review ****\r\n"); 170 1 : msg.append("\r\n"); 171 : 172 1 : Account account = user.getAccount(); 173 1 : String name = account.fullName(); 174 1 : if (name == null || name.isEmpty()) { 175 0 : name = user.getUserName().orElse(anonymousCowardName); 176 : } 177 1 : msg.append(" Hi "); 178 1 : msg.append(name); 179 1 : msg.append(", you have successfully connected over SSH."); 180 1 : msg.append("\r\n"); 181 1 : msg.append("\r\n"); 182 : 183 1 : msg.append(" Unfortunately, interactive shells are disabled.\r\n"); 184 1 : msg.append(" To clone a hosted Git repository, use:\r\n"); 185 1 : msg.append("\r\n"); 186 : 187 1 : if (!sshInfo.getHostKeys().isEmpty()) { 188 1 : String host = sshInfo.getHostKeys().get(0).getHost(); 189 1 : if (host.startsWith("*:")) { 190 0 : host = getGerritHost() + host.substring(1); 191 : } 192 : 193 1 : msg.append(" git clone ssh://"); 194 1 : if (user.getUserName().isPresent()) { 195 1 : msg.append(user.getUserName().get()); 196 1 : msg.append("@"); 197 : } 198 1 : msg.append(host); 199 1 : msg.append("/"); 200 1 : msg.append("REPOSITORY_NAME.git"); 201 1 : msg.append("\r\n"); 202 : } 203 : 204 1 : msg.append("\r\n"); 205 1 : return msg.toString(); 206 : } 207 : 208 : private String getGerritHost() { 209 0 : String url = urlProvider.get(); 210 0 : if (url != null) { 211 : try { 212 0 : return new URL(url).getHost(); 213 0 : } catch (MalformedURLException e) { 214 : // Ignored 215 : } 216 : } 217 0 : return SystemReader.getInstance().getHostname(); 218 : } 219 : } 220 : }