From 41124cddb6edd82c1630efb99b29c839304ed897 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Thu, 13 Jun 2013 17:03:24 -0500 Subject: Support serving repositories over the SSH transport Gitblit would greatly benefit from an integrated SSH server. This would complete the transport trifecta. Change-Id: I6fb95abe65655fa74d47ea71522d8d9a1541450c --- src/main/distrib/data/gitblit.properties | 17 + src/main/java/com/gitblit/GitBlitServer.java | 1408 ++++++++++---------- .../java/com/gitblit/manager/ServicesManager.java | 18 + .../gitblit/transport/ssh/AbstractSshCommand.java | 75 ++ .../gitblit/transport/ssh/SshCommandFactory.java | 164 +++ .../gitblit/transport/ssh/SshCommandServer.java | 217 +++ .../java/com/gitblit/transport/ssh/SshDaemon.java | 159 +++ .../com/gitblit/transport/ssh/SshDaemonClient.java | 37 + .../gitblit/transport/ssh/SshKeyAuthenticator.java | 43 + 9 files changed, 1436 insertions(+), 702 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshCommandServer.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshDaemon.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 3c605394..5bc28fd6 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -93,6 +93,23 @@ git.daemonBindInterface = # RESTART REQUIRED git.daemonPort = 9418 +# The port for serving the SSH service. <= 0 disables this service. +# On Unix/Linux systems, ports < 1024 require root permissions. +# Recommended value: 29418 +# +# SINCE 1.5.0 +# RESTART REQUIRED +git.sshPort = 29418 + +# Specify the interface for the SSH daemon to bind its service. +# You may specify an ip or an empty value to bind to all interfaces. +# Specifying localhost will result in Gitblit ONLY listening to requests to +# localhost. +# +# SINCE 1.5.0 +# RESTART REQUIRED +git.sshBindInterface = localhost + # Allow push/pull over http/https with JGit servlet. # If you do NOT want to allow Git clients to clone/push to Gitblit set this # to false. You might want to do this if you are only using ssh:// or git://. diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java index 64d3cadd..c37bc3a1 100644 --- a/src/main/java/com/gitblit/GitBlitServer.java +++ b/src/main/java/com/gitblit/GitBlitServer.java @@ -1,703 +1,707 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed 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 com.gitblit; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URI; -import java.net.URL; -import java.net.UnknownHostException; -import java.security.ProtectionDomain; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; -import java.util.Scanner; - -import org.apache.log4j.PropertyConfigurator; -import org.eclipse.jetty.ajp.Ajp13SocketConnector; -import org.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.security.ConstraintSecurityHandler; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.bio.SocketConnector; -import org.eclipse.jetty.server.nio.SelectChannelConnector; -import org.eclipse.jetty.server.session.HashSessionManager; -import org.eclipse.jetty.server.ssl.SslConnector; -import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; -import org.eclipse.jetty.server.ssl.SslSocketConnector; -import org.eclipse.jetty.util.security.Constraint; -import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.webapp.WebAppContext; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.FileUtils; -import org.kohsuke.args4j.CmdLineException; -import org.kohsuke.args4j.CmdLineParser; -import org.kohsuke.args4j.Option; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.authority.GitblitAuthority; -import com.gitblit.authority.NewCertificateConfig; -import com.gitblit.servlet.GitblitContext; -import com.gitblit.utils.StringUtils; -import com.gitblit.utils.TimeUtils; -import com.gitblit.utils.X509Utils; -import com.gitblit.utils.X509Utils.X509Log; -import com.gitblit.utils.X509Utils.X509Metadata; -import com.unboundid.ldap.listener.InMemoryDirectoryServer; -import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; -import com.unboundid.ldap.listener.InMemoryListenerConfig; -import com.unboundid.ldif.LDIFReader; - -/** - * GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts - * and stops an instance of Jetty that is configured from a combination of the - * gitblit.properties file and command line parameters. JCommander is used to - * simplify command line parameter processing. This class also automatically - * generates a self-signed certificate for localhost, if the keystore does not - * already exist. - * - * @author James Moger - * - */ -public class GitBlitServer { - - private static Logger logger; - - public static void main(String... args) { - GitBlitServer server = new GitBlitServer(); - - // filter out the baseFolder parameter - List filtered = new ArrayList(); - String folder = "data"; - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if (arg.equals("--baseFolder")) { - if (i + 1 == args.length) { - System.out.println("Invalid --baseFolder parameter!"); - System.exit(-1); - } else if (!".".equals(args[i + 1])) { - folder = args[i + 1]; - } - i = i + 1; - } else { - filtered.add(arg); - } - } - - Params.baseFolder = folder; - Params params = new Params(); - CmdLineParser parser = new CmdLineParser(params); - try { - parser.parseArgument(filtered); - if (params.help) { - server.usage(parser, null); - } - } catch (CmdLineException t) { - server.usage(parser, t); - } - - if (params.stop) { - server.stop(params); - } else { - server.start(params); - } - } - - /** - * Display the command line usage of Gitblit GO. - * - * @param parser - * @param t - */ - protected final void usage(CmdLineParser parser, CmdLineException t) { - System.out.println(Constants.BORDER); - System.out.println(Constants.getGitBlitVersion()); - System.out.println(Constants.BORDER); - System.out.println(); - if (t != null) { - System.out.println(t.getMessage()); - System.out.println(); - } - if (parser != null) { - parser.printUsage(System.out); - System.out - .println("\nExample:\n java -server -Xmx1024M -jar gitblit.jar --repositoriesFolder c:\\git --httpPort 80 --httpsPort 443"); - } - System.exit(0); - } - - /** - * Stop Gitblt GO. - */ - public void stop(Params params) { - try { - Socket s = new Socket(InetAddress.getByName("127.0.0.1"), params.shutdownPort); - OutputStream out = s.getOutputStream(); - System.out.println("Sending Shutdown Request to " + Constants.NAME); - out.write("\r\n".getBytes()); - out.flush(); - s.close(); - } catch (UnknownHostException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * Start Gitblit GO. - */ - protected final void start(Params params) { - final File baseFolder = new File(Params.baseFolder).getAbsoluteFile(); - FileSettings settings = params.FILESETTINGS; - if (!StringUtils.isEmpty(params.settingsfile)) { - if (new File(params.settingsfile).exists()) { - settings = new FileSettings(params.settingsfile); - } - } - - if (params.dailyLogFile) { - // Configure log4j for daily log file generation - InputStream is = null; - try { - is = getClass().getResourceAsStream("/log4j.properties"); - Properties loggingProperties = new Properties(); - loggingProperties.load(is); - - loggingProperties.put("log4j.appender.R.File", new File(baseFolder, "logs/gitblit.log").getAbsolutePath()); - loggingProperties.put("log4j.rootCategory", "INFO, R"); - - if (settings.getBoolean(Keys.web.debugMode, false)) { - loggingProperties.put("log4j.logger.com.gitblit", "DEBUG"); - } - - PropertyConfigurator.configure(loggingProperties); - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - is.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - logger = LoggerFactory.getLogger(GitBlitServer.class); - logger.info(Constants.BORDER); - logger.info(" _____ _ _ _ _ _ _"); - logger.info(" | __ \\(_)| | | | | |(_)| |"); - logger.info(" | | \\/ _ | |_ | |__ | | _ | |_"); - logger.info(" | | __ | || __|| '_ \\ | || || __|"); - logger.info(" | |_\\ \\| || |_ | |_) || || || |_"); - logger.info(" \\____/|_| \\__||_.__/ |_||_| \\__|"); - int spacing = (Constants.BORDER.length() - Constants.getGitBlitVersion().length()) / 2; - StringBuilder sb = new StringBuilder(); - while (spacing > 0) { - spacing--; - sb.append(' '); - } - logger.info(sb.toString() + Constants.getGitBlitVersion()); - logger.info(""); - logger.info(Constants.BORDER); - - System.setProperty("java.awt.headless", "true"); - - String osname = System.getProperty("os.name"); - String osversion = System.getProperty("os.version"); - logger.info("Running on " + osname + " (" + osversion + ")"); - - List connectors = new ArrayList(); - - // conditionally configure the http connector - if (params.port > 0) { - Connector httpConnector = createConnector(params.useNIO, params.port, settings.getInteger(Keys.server.threadPoolSize, 50)); - String bindInterface = settings.getString(Keys.server.httpBindInterface, null); - if (!StringUtils.isEmpty(bindInterface)) { - logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}", - params.port, bindInterface)); - httpConnector.setHost(bindInterface); - } - if (params.port < 1024 && !isWindows()) { - logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); - } - if (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) { - // redirect HTTP requests to HTTPS - if (httpConnector instanceof SelectChannelConnector) { - ((SelectChannelConnector) httpConnector).setConfidentialPort(params.securePort); - } else { - ((SocketConnector) httpConnector).setConfidentialPort(params.securePort); - } - } - connectors.add(httpConnector); - } - - // conditionally configure the https connector - if (params.securePort > 0) { - File certificatesConf = new File(baseFolder, X509Utils.CA_CONFIG); - File serverKeyStore = new File(baseFolder, X509Utils.SERVER_KEY_STORE); - File serverTrustStore = new File(baseFolder, X509Utils.SERVER_TRUST_STORE); - File caRevocationList = new File(baseFolder, X509Utils.CA_REVOCATION_LIST); - - // generate CA & web certificates, create certificate stores - X509Metadata metadata = new X509Metadata("localhost", params.storePassword); - // set default certificate values from config file - if (certificatesConf.exists()) { - FileBasedConfig config = new FileBasedConfig(certificatesConf, FS.detect()); - try { - config.load(); - } catch (Exception e) { - logger.error("Error parsing " + certificatesConf, e); - } - NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config); - certificateConfig.update(metadata); - } - - metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR); - X509Utils.prepareX509Infrastructure(metadata, baseFolder, new X509Log() { - @Override - public void log(String message) { - BufferedWriter writer = null; - try { - writer = new BufferedWriter(new FileWriter(new File(baseFolder, X509Utils.CERTS + File.separator + "log.txt"), true)); - writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message)); - writer.newLine(); - writer.flush(); - } catch (Exception e) { - LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - } - } - } - } - }); - - if (serverKeyStore.exists()) { - Connector secureConnector = createSSLConnector(params.alias, serverKeyStore, serverTrustStore, params.storePassword, - caRevocationList, params.useNIO, params.securePort, settings.getInteger(Keys.server.threadPoolSize, 50), params.requireClientCertificates); - String bindInterface = settings.getString(Keys.server.httpsBindInterface, null); - if (!StringUtils.isEmpty(bindInterface)) { - logger.warn(MessageFormat.format( - "Binding ssl connector on port {0,number,0} to {1}", params.securePort, - bindInterface)); - secureConnector.setHost(bindInterface); - } - if (params.securePort < 1024 && !isWindows()) { - logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); - } - connectors.add(secureConnector); - } else { - logger.warn("Failed to find or load Keystore?"); - logger.warn("SSL connector DISABLED."); - } - } - - // conditionally configure the ajp connector - if (params.ajpPort > 0) { - Connector ajpConnector = createAJPConnector(params.ajpPort); - String bindInterface = settings.getString(Keys.server.ajpBindInterface, null); - if (!StringUtils.isEmpty(bindInterface)) { - logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}", - params.ajpPort, bindInterface)); - ajpConnector.setHost(bindInterface); - } - if (params.ajpPort < 1024 && !isWindows()) { - logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); - } - connectors.add(ajpConnector); - } - - // tempDir is where the embedded Gitblit web application is expanded and - // where Jetty creates any necessary temporary files - File tempDir = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.temp); - if (tempDir.exists()) { - try { - FileUtils.delete(tempDir, FileUtils.RECURSIVE | FileUtils.RETRY); - } catch (IOException x) { - logger.warn("Failed to delete temp dir " + tempDir.getAbsolutePath(), x); - } - } - if (!tempDir.mkdirs()) { - logger.warn("Failed to create temp dir " + tempDir.getAbsolutePath()); - } - - Server server = new Server(); - server.setStopAtShutdown(true); - server.setConnectors(connectors.toArray(new Connector[connectors.size()])); - - // Get the execution path of this class - // We use this to set the WAR path. - ProtectionDomain protectionDomain = GitBlitServer.class.getProtectionDomain(); - URL location = protectionDomain.getCodeSource().getLocation(); - - // Root WebApp Context - WebAppContext rootContext = new WebAppContext(); - rootContext.setContextPath(settings.getString(Keys.server.contextPath, "/")); - rootContext.setServer(server); - rootContext.setWar(location.toExternalForm()); - rootContext.setTempDirectory(tempDir); - - // Set cookies HttpOnly so they are not accessible to JavaScript engines - HashSessionManager sessionManager = new HashSessionManager(); - sessionManager.setHttpOnly(true); - // Use secure cookies if only serving https - sessionManager.setSecureRequestOnly(params.port <= 0 && params.securePort > 0); - rootContext.getSessionHandler().setSessionManager(sessionManager); - - // Ensure there is a defined User Service - String realmUsers = params.userService; - if (StringUtils.isEmpty(realmUsers)) { - logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService)); - return; - } - - // Override settings from the command-line - settings.overrideSetting(Keys.realm.userService, params.userService); - settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder); - settings.overrideSetting(Keys.git.daemonPort, params.gitPort); - - // Start up an in-memory LDAP server, if configured - try { - if (!StringUtils.isEmpty(params.ldapLdifFile)) { - File ldifFile = new File(params.ldapLdifFile); - if (ldifFile != null && ldifFile.exists()) { - URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); - String firstLine = new Scanner(ldifFile).nextLine(); - String rootDN = firstLine.substring(4); - String bindUserName = settings.getString(Keys.realm.ldap.username, ""); - String bindPassword = settings.getString(Keys.realm.ldap.password, ""); - - // Get the port - int port = ldapUrl.getPort(); - if (port == -1) - port = 389; - - InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN); - config.addAdditionalBindCredentials(bindUserName, bindPassword); - config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", port)); - config.setSchema(null); - - InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); - ds.importFromLDIF(true, new LDIFReader(ldifFile)); - ds.startListening(); - - logger.info("LDAP Server started at ldap://localhost:" + port); - } - } - } catch (Exception e) { - // Completely optional, just show a warning - logger.warn("Unable to start LDAP server", e); - } - - // Set the server's contexts - server.setHandler(rootContext); - - // redirect HTTP requests to HTTPS - if (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) { - logger.info(String.format("Configuring automatic http(%1$s) -> https(%2$s) redirects", params.port, params.securePort)); - // Create the internal mechanisms to handle secure connections and redirects - Constraint constraint = new Constraint(); - constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL); - - ConstraintMapping cm = new ConstraintMapping(); - cm.setConstraint(constraint); - cm.setPathSpec("/*"); - - ConstraintSecurityHandler sh = new ConstraintSecurityHandler(); - sh.setConstraintMappings(new ConstraintMapping[] { cm }); - - // Configure this context to use the Security Handler defined before - rootContext.setHandler(sh); - } - - // Setup the Gitblit context - GitblitContext gitblit = newGitblit(settings, baseFolder); - rootContext.addEventListener(gitblit); - - try { - // start the shutdown monitor - if (params.shutdownPort > 0) { - Thread shutdownMonitor = new ShutdownMonitorThread(server, params); - shutdownMonitor.start(); - } - - // start Jetty - server.start(); - server.join(); - } catch (Exception e) { - e.printStackTrace(); - System.exit(100); - } - } - - protected GitblitContext newGitblit(IStoredSettings settings, File baseFolder) { - return new GitblitContext(settings, baseFolder); - } - - /** - * Creates an http connector. - * - * @param useNIO - * @param port - * @param threadPoolSize - * @return an http connector - */ - private Connector createConnector(boolean useNIO, int port, int threadPoolSize) { - Connector connector; - if (useNIO) { - logger.info("Setting up NIO SelectChannelConnector on port " + port); - SelectChannelConnector nioconn = new SelectChannelConnector(); - nioconn.setSoLingerTime(-1); - if (threadPoolSize > 0) { - nioconn.setThreadPool(new QueuedThreadPool(threadPoolSize)); - } - connector = nioconn; - } else { - logger.info("Setting up SocketConnector on port " + port); - SocketConnector sockconn = new SocketConnector(); - if (threadPoolSize > 0) { - sockconn.setThreadPool(new QueuedThreadPool(threadPoolSize)); - } - connector = sockconn; - } - - connector.setPort(port); - connector.setMaxIdleTime(30000); - return connector; - } - - /** - * Creates an https connector. - * - * SSL renegotiation will be enabled if the JVM is 1.6.0_22 or later. - * oracle.com/technetwork/java/javase/documentation/tlsreadme2-176330.html - * - * @param certAlias - * @param keyStore - * @param clientTrustStore - * @param storePassword - * @param caRevocationList - * @param useNIO - * @param port - * @param threadPoolSize - * @param requireClientCertificates - * @return an https connector - */ - private Connector createSSLConnector(String certAlias, File keyStore, File clientTrustStore, - String storePassword, File caRevocationList, boolean useNIO, int port, int threadPoolSize, - boolean requireClientCertificates) { - GitblitSslContextFactory factory = new GitblitSslContextFactory(certAlias, - keyStore, clientTrustStore, storePassword, caRevocationList); - SslConnector connector; - if (useNIO) { - logger.info("Setting up NIO SslSelectChannelConnector on port " + port); - SslSelectChannelConnector ssl = new SslSelectChannelConnector(factory); - ssl.setSoLingerTime(-1); - if (requireClientCertificates) { - factory.setNeedClientAuth(true); - } else { - factory.setWantClientAuth(true); - } - if (threadPoolSize > 0) { - ssl.setThreadPool(new QueuedThreadPool(threadPoolSize)); - } - connector = ssl; - } else { - logger.info("Setting up NIO SslSocketConnector on port " + port); - SslSocketConnector ssl = new SslSocketConnector(factory); - if (threadPoolSize > 0) { - ssl.setThreadPool(new QueuedThreadPool(threadPoolSize)); - } - connector = ssl; - } - connector.setPort(port); - connector.setMaxIdleTime(30000); - - return connector; - } - - /** - * Creates an ajp connector. - * - * @param port - * @return an ajp connector - */ - private Connector createAJPConnector(int port) { - logger.info("Setting up AJP Connector on port " + port); - Ajp13SocketConnector ajp = new Ajp13SocketConnector(); - ajp.setPort(port); - if (port < 1024 && !isWindows()) { - logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); - } - return ajp; - } - - /** - * Tests to see if the operating system is Windows. - * - * @return true if this is a windows machine - */ - private boolean isWindows() { - return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1; - } - - /** - * The ShutdownMonitorThread opens a socket on a specified port and waits - * for an incoming connection. When that connection is accepted a shutdown - * message is issued to the running Jetty server. - * - * @author James Moger - * - */ - private static class ShutdownMonitorThread extends Thread { - - private final ServerSocket socket; - - private final Server server; - - private final Logger logger = LoggerFactory.getLogger(ShutdownMonitorThread.class); - - public ShutdownMonitorThread(Server server, Params params) { - this.server = server; - setDaemon(true); - setName(Constants.NAME + " Shutdown Monitor"); - ServerSocket skt = null; - try { - skt = new ServerSocket(params.shutdownPort, 1, InetAddress.getByName("127.0.0.1")); - } catch (Exception e) { - logger.warn("Could not open shutdown monitor on port " + params.shutdownPort, e); - } - socket = skt; - } - - @Override - public void run() { - logger.info("Shutdown Monitor listening on port " + socket.getLocalPort()); - Socket accept; - try { - accept = socket.accept(); - BufferedReader reader = new BufferedReader(new InputStreamReader( - accept.getInputStream())); - reader.readLine(); - logger.info(Constants.BORDER); - logger.info("Stopping " + Constants.NAME); - logger.info(Constants.BORDER); - server.stop(); - server.setStopAtShutdown(false); - accept.close(); - socket.close(); - } catch (Exception e) { - logger.warn("Failed to shutdown Jetty", e); - } - } - } - - /** - * Parameters class for GitBlitServer. - */ - public static class Params { - - public static String baseFolder; - - private final FileSettings FILESETTINGS = new FileSettings(new File(baseFolder, Constants.PROPERTIES_FILE).getAbsolutePath()); - - /* - * Server parameters - */ - @Option(name = "--help", aliases = { "-h"}, usage = "Show this help") - public Boolean help = false; - - @Option(name = "--stop", usage = "Stop Server") - public Boolean stop = false; - - @Option(name = "--tempFolder", usage = "Folder for server to extract built-in webapp", metaVar="PATH") - public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp"); - - @Option(name = "--dailyLogFile", usage = "Log to a rolling daily log file INSTEAD of stdout.") - public Boolean dailyLogFile = false; - - /* - * GIT Servlet Parameters - */ - @Option(name = "--repositoriesFolder", usage = "Git Repositories Folder", metaVar="PATH") - public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder, - "git"); - - /* - * Authentication Parameters - */ - @Option(name = "--userService", usage = "Authentication and Authorization Service (filename or fully qualified classname)") - public String userService = FILESETTINGS.getString(Keys.realm.userService, - "users.conf"); - - /* - * JETTY Parameters - */ - @Option(name = "--useNio", usage = "Use NIO Connector else use Socket Connector.") - public Boolean useNIO = FILESETTINGS.getBoolean(Keys.server.useNio, true); - - @Option(name = "--httpPort", usage = "HTTP port for to serve. (port <= 0 will disable this connector)", metaVar="PORT") - public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 0); - - @Option(name = "--httpsPort", usage = "HTTPS port to serve. (port <= 0 will disable this connector)", metaVar="PORT") - public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 8443); - - @Option(name = "--ajpPort", usage = "AJP port to serve. (port <= 0 will disable this connector)", metaVar="PORT") - public Integer ajpPort = FILESETTINGS.getInteger(Keys.server.ajpPort, 0); - - @Option(name = "--gitPort", usage = "Git Daemon port to serve. (port <= 0 will disable this connector)", metaVar="PORT") - public Integer gitPort = FILESETTINGS.getInteger(Keys.git.daemonPort, 9418); - - @Option(name = "--alias", usage = "Alias of SSL certificate in keystore for serving https.", metaVar="ALIAS") - public String alias = FILESETTINGS.getString(Keys.server.certificateAlias, ""); - - @Option(name = "--storePassword", usage = "Password for SSL (https) keystore.", metaVar="PASSWORD") - public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, ""); - - @Option(name = "--shutdownPort", usage = "Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)", metaVar="PORT") - public Integer shutdownPort = FILESETTINGS.getInteger(Keys.server.shutdownPort, 8081); - - @Option(name = "--requireClientCertificates", usage = "Require client X509 certificates for https connections.") - public Boolean requireClientCertificates = FILESETTINGS.getBoolean(Keys.server.requireClientCertificates, false); - - /* - * Setting overrides - */ - @Option(name = "--settings", usage = "Path to alternative settings", metaVar="FILE") - public String settingsfile; - - @Option(name = "--ldapLdifFile", usage = "Path to LDIF file. This will cause an in-memory LDAP server to be started according to gitblit settings", metaVar="FILE") - public String ldapLdifFile; - - } +/* + * Copyright 2011 gitblit.com. + * + * Licensed 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 com.gitblit; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.ProtectionDomain; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.Scanner; + +import org.apache.log4j.PropertyConfigurator; +import org.eclipse.jetty.ajp.Ajp13SocketConnector; +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.bio.SocketConnector; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.server.session.HashSessionManager; +import org.eclipse.jetty.server.ssl.SslConnector; +import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; +import org.eclipse.jetty.server.ssl.SslSocketConnector; +import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.authority.GitblitAuthority; +import com.gitblit.authority.NewCertificateConfig; +import com.gitblit.servlet.GitblitContext; +import com.gitblit.utils.StringUtils; +import com.gitblit.utils.TimeUtils; +import com.gitblit.utils.X509Utils; +import com.gitblit.utils.X509Utils.X509Log; +import com.gitblit.utils.X509Utils.X509Metadata; +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.listener.InMemoryListenerConfig; +import com.unboundid.ldif.LDIFReader; + +/** + * GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts + * and stops an instance of Jetty that is configured from a combination of the + * gitblit.properties file and command line parameters. JCommander is used to + * simplify command line parameter processing. This class also automatically + * generates a self-signed certificate for localhost, if the keystore does not + * already exist. + * + * @author James Moger + * + */ +public class GitBlitServer { + + private static Logger logger; + + public static void main(String... args) { + GitBlitServer server = new GitBlitServer(); + + // filter out the baseFolder parameter + List filtered = new ArrayList(); + String folder = "data"; + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.equals("--baseFolder")) { + if (i + 1 == args.length) { + System.out.println("Invalid --baseFolder parameter!"); + System.exit(-1); + } else if (!".".equals(args[i + 1])) { + folder = args[i + 1]; + } + i = i + 1; + } else { + filtered.add(arg); + } + } + + Params.baseFolder = folder; + Params params = new Params(); + CmdLineParser parser = new CmdLineParser(params); + try { + parser.parseArgument(filtered); + if (params.help) { + server.usage(parser, null); + } + } catch (CmdLineException t) { + server.usage(parser, t); + } + + if (params.stop) { + server.stop(params); + } else { + server.start(params); + } + } + + /** + * Display the command line usage of Gitblit GO. + * + * @param parser + * @param t + */ + protected final void usage(CmdLineParser parser, CmdLineException t) { + System.out.println(Constants.BORDER); + System.out.println(Constants.getGitBlitVersion()); + System.out.println(Constants.BORDER); + System.out.println(); + if (t != null) { + System.out.println(t.getMessage()); + System.out.println(); + } + if (parser != null) { + parser.printUsage(System.out); + System.out + .println("\nExample:\n java -server -Xmx1024M -jar gitblit.jar --repositoriesFolder c:\\git --httpPort 80 --httpsPort 443"); + } + System.exit(0); + } + + /** + * Stop Gitblt GO. + */ + public void stop(Params params) { + try { + Socket s = new Socket(InetAddress.getByName("127.0.0.1"), params.shutdownPort); + OutputStream out = s.getOutputStream(); + System.out.println("Sending Shutdown Request to " + Constants.NAME); + out.write("\r\n".getBytes()); + out.flush(); + s.close(); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Start Gitblit GO. + */ + protected final void start(Params params) { + final File baseFolder = new File(Params.baseFolder).getAbsoluteFile(); + FileSettings settings = params.FILESETTINGS; + if (!StringUtils.isEmpty(params.settingsfile)) { + if (new File(params.settingsfile).exists()) { + settings = new FileSettings(params.settingsfile); + } + } + + if (params.dailyLogFile) { + // Configure log4j for daily log file generation + InputStream is = null; + try { + is = getClass().getResourceAsStream("/log4j.properties"); + Properties loggingProperties = new Properties(); + loggingProperties.load(is); + + loggingProperties.put("log4j.appender.R.File", new File(baseFolder, "logs/gitblit.log").getAbsolutePath()); + loggingProperties.put("log4j.rootCategory", "INFO, R"); + + if (settings.getBoolean(Keys.web.debugMode, false)) { + loggingProperties.put("log4j.logger.com.gitblit", "DEBUG"); + } + + PropertyConfigurator.configure(loggingProperties); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + logger = LoggerFactory.getLogger(GitBlitServer.class); + logger.info(Constants.BORDER); + logger.info(" _____ _ _ _ _ _ _"); + logger.info(" | __ \\(_)| | | | | |(_)| |"); + logger.info(" | | \\/ _ | |_ | |__ | | _ | |_"); + logger.info(" | | __ | || __|| '_ \\ | || || __|"); + logger.info(" | |_\\ \\| || |_ | |_) || || || |_"); + logger.info(" \\____/|_| \\__||_.__/ |_||_| \\__|"); + int spacing = (Constants.BORDER.length() - Constants.getGitBlitVersion().length()) / 2; + StringBuilder sb = new StringBuilder(); + while (spacing > 0) { + spacing--; + sb.append(' '); + } + logger.info(sb.toString() + Constants.getGitBlitVersion()); + logger.info(""); + logger.info(Constants.BORDER); + + System.setProperty("java.awt.headless", "true"); + + String osname = System.getProperty("os.name"); + String osversion = System.getProperty("os.version"); + logger.info("Running on " + osname + " (" + osversion + ")"); + + List connectors = new ArrayList(); + + // conditionally configure the http connector + if (params.port > 0) { + Connector httpConnector = createConnector(params.useNIO, params.port, settings.getInteger(Keys.server.threadPoolSize, 50)); + String bindInterface = settings.getString(Keys.server.httpBindInterface, null); + if (!StringUtils.isEmpty(bindInterface)) { + logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}", + params.port, bindInterface)); + httpConnector.setHost(bindInterface); + } + if (params.port < 1024 && !isWindows()) { + logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); + } + if (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) { + // redirect HTTP requests to HTTPS + if (httpConnector instanceof SelectChannelConnector) { + ((SelectChannelConnector) httpConnector).setConfidentialPort(params.securePort); + } else { + ((SocketConnector) httpConnector).setConfidentialPort(params.securePort); + } + } + connectors.add(httpConnector); + } + + // conditionally configure the https connector + if (params.securePort > 0) { + File certificatesConf = new File(baseFolder, X509Utils.CA_CONFIG); + File serverKeyStore = new File(baseFolder, X509Utils.SERVER_KEY_STORE); + File serverTrustStore = new File(baseFolder, X509Utils.SERVER_TRUST_STORE); + File caRevocationList = new File(baseFolder, X509Utils.CA_REVOCATION_LIST); + + // generate CA & web certificates, create certificate stores + X509Metadata metadata = new X509Metadata("localhost", params.storePassword); + // set default certificate values from config file + if (certificatesConf.exists()) { + FileBasedConfig config = new FileBasedConfig(certificatesConf, FS.detect()); + try { + config.load(); + } catch (Exception e) { + logger.error("Error parsing " + certificatesConf, e); + } + NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config); + certificateConfig.update(metadata); + } + + metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR); + X509Utils.prepareX509Infrastructure(metadata, baseFolder, new X509Log() { + @Override + public void log(String message) { + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new FileWriter(new File(baseFolder, X509Utils.CERTS + File.separator + "log.txt"), true)); + writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message)); + writer.newLine(); + writer.flush(); + } catch (Exception e) { + LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + } + } + } + } + }); + + if (serverKeyStore.exists()) { + Connector secureConnector = createSSLConnector(params.alias, serverKeyStore, serverTrustStore, params.storePassword, + caRevocationList, params.useNIO, params.securePort, settings.getInteger(Keys.server.threadPoolSize, 50), params.requireClientCertificates); + String bindInterface = settings.getString(Keys.server.httpsBindInterface, null); + if (!StringUtils.isEmpty(bindInterface)) { + logger.warn(MessageFormat.format( + "Binding ssl connector on port {0,number,0} to {1}", params.securePort, + bindInterface)); + secureConnector.setHost(bindInterface); + } + if (params.securePort < 1024 && !isWindows()) { + logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); + } + connectors.add(secureConnector); + } else { + logger.warn("Failed to find or load Keystore?"); + logger.warn("SSL connector DISABLED."); + } + } + + // conditionally configure the ajp connector + if (params.ajpPort > 0) { + Connector ajpConnector = createAJPConnector(params.ajpPort); + String bindInterface = settings.getString(Keys.server.ajpBindInterface, null); + if (!StringUtils.isEmpty(bindInterface)) { + logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}", + params.ajpPort, bindInterface)); + ajpConnector.setHost(bindInterface); + } + if (params.ajpPort < 1024 && !isWindows()) { + logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); + } + connectors.add(ajpConnector); + } + + // tempDir is where the embedded Gitblit web application is expanded and + // where Jetty creates any necessary temporary files + File tempDir = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.temp); + if (tempDir.exists()) { + try { + FileUtils.delete(tempDir, FileUtils.RECURSIVE | FileUtils.RETRY); + } catch (IOException x) { + logger.warn("Failed to delete temp dir " + tempDir.getAbsolutePath(), x); + } + } + if (!tempDir.mkdirs()) { + logger.warn("Failed to create temp dir " + tempDir.getAbsolutePath()); + } + + Server server = new Server(); + server.setStopAtShutdown(true); + server.setConnectors(connectors.toArray(new Connector[connectors.size()])); + + // Get the execution path of this class + // We use this to set the WAR path. + ProtectionDomain protectionDomain = GitBlitServer.class.getProtectionDomain(); + URL location = protectionDomain.getCodeSource().getLocation(); + + // Root WebApp Context + WebAppContext rootContext = new WebAppContext(); + rootContext.setContextPath(settings.getString(Keys.server.contextPath, "/")); + rootContext.setServer(server); + rootContext.setWar(location.toExternalForm()); + rootContext.setTempDirectory(tempDir); + + // Set cookies HttpOnly so they are not accessible to JavaScript engines + HashSessionManager sessionManager = new HashSessionManager(); + sessionManager.setHttpOnly(true); + // Use secure cookies if only serving https + sessionManager.setSecureRequestOnly(params.port <= 0 && params.securePort > 0); + rootContext.getSessionHandler().setSessionManager(sessionManager); + + // Ensure there is a defined User Service + String realmUsers = params.userService; + if (StringUtils.isEmpty(realmUsers)) { + logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService)); + return; + } + + // Override settings from the command-line + settings.overrideSetting(Keys.realm.userService, params.userService); + settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder); + settings.overrideSetting(Keys.git.daemonPort, params.gitPort); + settings.overrideSetting(Keys.git.sshPort, params.sshPort); + + // Start up an in-memory LDAP server, if configured + try { + if (!StringUtils.isEmpty(params.ldapLdifFile)) { + File ldifFile = new File(params.ldapLdifFile); + if (ldifFile != null && ldifFile.exists()) { + URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server)); + String firstLine = new Scanner(ldifFile).nextLine(); + String rootDN = firstLine.substring(4); + String bindUserName = settings.getString(Keys.realm.ldap.username, ""); + String bindPassword = settings.getString(Keys.realm.ldap.password, ""); + + // Get the port + int port = ldapUrl.getPort(); + if (port == -1) + port = 389; + + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN); + config.addAdditionalBindCredentials(bindUserName, bindPassword); + config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", port)); + config.setSchema(null); + + InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); + ds.importFromLDIF(true, new LDIFReader(ldifFile)); + ds.startListening(); + + logger.info("LDAP Server started at ldap://localhost:" + port); + } + } + } catch (Exception e) { + // Completely optional, just show a warning + logger.warn("Unable to start LDAP server", e); + } + + // Set the server's contexts + server.setHandler(rootContext); + + // redirect HTTP requests to HTTPS + if (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) { + logger.info(String.format("Configuring automatic http(%1$s) -> https(%2$s) redirects", params.port, params.securePort)); + // Create the internal mechanisms to handle secure connections and redirects + Constraint constraint = new Constraint(); + constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL); + + ConstraintMapping cm = new ConstraintMapping(); + cm.setConstraint(constraint); + cm.setPathSpec("/*"); + + ConstraintSecurityHandler sh = new ConstraintSecurityHandler(); + sh.setConstraintMappings(new ConstraintMapping[] { cm }); + + // Configure this context to use the Security Handler defined before + rootContext.setHandler(sh); + } + + // Setup the Gitblit context + GitblitContext gitblit = newGitblit(settings, baseFolder); + rootContext.addEventListener(gitblit); + + try { + // start the shutdown monitor + if (params.shutdownPort > 0) { + Thread shutdownMonitor = new ShutdownMonitorThread(server, params); + shutdownMonitor.start(); + } + + // start Jetty + server.start(); + server.join(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(100); + } + } + + protected GitblitContext newGitblit(IStoredSettings settings, File baseFolder) { + return new GitblitContext(settings, baseFolder); + } + + /** + * Creates an http connector. + * + * @param useNIO + * @param port + * @param threadPoolSize + * @return an http connector + */ + private Connector createConnector(boolean useNIO, int port, int threadPoolSize) { + Connector connector; + if (useNIO) { + logger.info("Setting up NIO SelectChannelConnector on port " + port); + SelectChannelConnector nioconn = new SelectChannelConnector(); + nioconn.setSoLingerTime(-1); + if (threadPoolSize > 0) { + nioconn.setThreadPool(new QueuedThreadPool(threadPoolSize)); + } + connector = nioconn; + } else { + logger.info("Setting up SocketConnector on port " + port); + SocketConnector sockconn = new SocketConnector(); + if (threadPoolSize > 0) { + sockconn.setThreadPool(new QueuedThreadPool(threadPoolSize)); + } + connector = sockconn; + } + + connector.setPort(port); + connector.setMaxIdleTime(30000); + return connector; + } + + /** + * Creates an https connector. + * + * SSL renegotiation will be enabled if the JVM is 1.6.0_22 or later. + * oracle.com/technetwork/java/javase/documentation/tlsreadme2-176330.html + * + * @param certAlias + * @param keyStore + * @param clientTrustStore + * @param storePassword + * @param caRevocationList + * @param useNIO + * @param port + * @param threadPoolSize + * @param requireClientCertificates + * @return an https connector + */ + private Connector createSSLConnector(String certAlias, File keyStore, File clientTrustStore, + String storePassword, File caRevocationList, boolean useNIO, int port, int threadPoolSize, + boolean requireClientCertificates) { + GitblitSslContextFactory factory = new GitblitSslContextFactory(certAlias, + keyStore, clientTrustStore, storePassword, caRevocationList); + SslConnector connector; + if (useNIO) { + logger.info("Setting up NIO SslSelectChannelConnector on port " + port); + SslSelectChannelConnector ssl = new SslSelectChannelConnector(factory); + ssl.setSoLingerTime(-1); + if (requireClientCertificates) { + factory.setNeedClientAuth(true); + } else { + factory.setWantClientAuth(true); + } + if (threadPoolSize > 0) { + ssl.setThreadPool(new QueuedThreadPool(threadPoolSize)); + } + connector = ssl; + } else { + logger.info("Setting up NIO SslSocketConnector on port " + port); + SslSocketConnector ssl = new SslSocketConnector(factory); + if (threadPoolSize > 0) { + ssl.setThreadPool(new QueuedThreadPool(threadPoolSize)); + } + connector = ssl; + } + connector.setPort(port); + connector.setMaxIdleTime(30000); + + return connector; + } + + /** + * Creates an ajp connector. + * + * @param port + * @return an ajp connector + */ + private Connector createAJPConnector(int port) { + logger.info("Setting up AJP Connector on port " + port); + Ajp13SocketConnector ajp = new Ajp13SocketConnector(); + ajp.setPort(port); + if (port < 1024 && !isWindows()) { + logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!"); + } + return ajp; + } + + /** + * Tests to see if the operating system is Windows. + * + * @return true if this is a windows machine + */ + private boolean isWindows() { + return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1; + } + + /** + * The ShutdownMonitorThread opens a socket on a specified port and waits + * for an incoming connection. When that connection is accepted a shutdown + * message is issued to the running Jetty server. + * + * @author James Moger + * + */ + private static class ShutdownMonitorThread extends Thread { + + private final ServerSocket socket; + + private final Server server; + + private final Logger logger = LoggerFactory.getLogger(ShutdownMonitorThread.class); + + public ShutdownMonitorThread(Server server, Params params) { + this.server = server; + setDaemon(true); + setName(Constants.NAME + " Shutdown Monitor"); + ServerSocket skt = null; + try { + skt = new ServerSocket(params.shutdownPort, 1, InetAddress.getByName("127.0.0.1")); + } catch (Exception e) { + logger.warn("Could not open shutdown monitor on port " + params.shutdownPort, e); + } + socket = skt; + } + + @Override + public void run() { + logger.info("Shutdown Monitor listening on port " + socket.getLocalPort()); + Socket accept; + try { + accept = socket.accept(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + accept.getInputStream())); + reader.readLine(); + logger.info(Constants.BORDER); + logger.info("Stopping " + Constants.NAME); + logger.info(Constants.BORDER); + server.stop(); + server.setStopAtShutdown(false); + accept.close(); + socket.close(); + } catch (Exception e) { + logger.warn("Failed to shutdown Jetty", e); + } + } + } + + /** + * Parameters class for GitBlitServer. + */ + public static class Params { + + public static String baseFolder; + + private final FileSettings FILESETTINGS = new FileSettings(new File(baseFolder, Constants.PROPERTIES_FILE).getAbsolutePath()); + + /* + * Server parameters + */ + @Option(name = "--help", aliases = { "-h"}, usage = "Show this help") + public Boolean help = false; + + @Option(name = "--stop", usage = "Stop Server") + public Boolean stop = false; + + @Option(name = "--tempFolder", usage = "Folder for server to extract built-in webapp", metaVar="PATH") + public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp"); + + @Option(name = "--dailyLogFile", usage = "Log to a rolling daily log file INSTEAD of stdout.") + public Boolean dailyLogFile = false; + + /* + * GIT Servlet Parameters + */ + @Option(name = "--repositoriesFolder", usage = "Git Repositories Folder", metaVar="PATH") + public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder, + "git"); + + /* + * Authentication Parameters + */ + @Option(name = "--userService", usage = "Authentication and Authorization Service (filename or fully qualified classname)") + public String userService = FILESETTINGS.getString(Keys.realm.userService, + "users.conf"); + + /* + * JETTY Parameters + */ + @Option(name = "--useNio", usage = "Use NIO Connector else use Socket Connector.") + public Boolean useNIO = FILESETTINGS.getBoolean(Keys.server.useNio, true); + + @Option(name = "--httpPort", usage = "HTTP port for to serve. (port <= 0 will disable this connector)", metaVar="PORT") + public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 0); + + @Option(name = "--httpsPort", usage = "HTTPS port to serve. (port <= 0 will disable this connector)", metaVar="PORT") + public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 8443); + + @Option(name = "--ajpPort", usage = "AJP port to serve. (port <= 0 will disable this connector)", metaVar="PORT") + public Integer ajpPort = FILESETTINGS.getInteger(Keys.server.ajpPort, 0); + + @Option(name = "--gitPort", usage = "Git Daemon port to serve. (port <= 0 will disable this connector)", metaVar="PORT") + public Integer gitPort = FILESETTINGS.getInteger(Keys.git.daemonPort, 9418); + + @Option(name = "--sshPort", usage = "Git SSH port to serve. (port <= 0 will disable this connector)", metaVar = "PORT") + public Integer sshPort = FILESETTINGS.getInteger(Keys.git.sshPort, 29418); + + @Option(name = "--alias", usage = "Alias of SSL certificate in keystore for serving https.", metaVar="ALIAS") + public String alias = FILESETTINGS.getString(Keys.server.certificateAlias, ""); + + @Option(name = "--storePassword", usage = "Password for SSL (https) keystore.", metaVar="PASSWORD") + public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, ""); + + @Option(name = "--shutdownPort", usage = "Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)", metaVar="PORT") + public Integer shutdownPort = FILESETTINGS.getInteger(Keys.server.shutdownPort, 8081); + + @Option(name = "--requireClientCertificates", usage = "Require client X509 certificates for https connections.") + public Boolean requireClientCertificates = FILESETTINGS.getBoolean(Keys.server.requireClientCertificates, false); + + /* + * Setting overrides + */ + @Option(name = "--settings", usage = "Path to alternative settings", metaVar="FILE") + public String settingsfile; + + @Option(name = "--ldapLdifFile", usage = "Path to LDIF file. This will cause an in-memory LDAP server to be started according to gitblit settings", metaVar="FILE") + public String ldapLdifFile; + + } } \ No newline at end of file diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index 8107a7d8..2cd583ad 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -42,6 +42,7 @@ import com.gitblit.models.FederationModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.service.FederationPullService; +import com.gitblit.transport.ssh.SshDaemon; import com.gitblit.utils.StringUtils; import com.gitblit.utils.TimeUtils; @@ -67,6 +68,8 @@ public class ServicesManager implements IManager { private GitDaemon gitDaemon; + private SshDaemon sshDaemon; + public ServicesManager(IGitblit gitblit) { this.settings = gitblit.getSettings(); this.gitblit = gitblit; @@ -77,6 +80,7 @@ public class ServicesManager implements IManager { configureFederation(); configureFanout(); configureGitDaemon(); + configureSshDaemon(); return this; } @@ -138,6 +142,20 @@ public class ServicesManager implements IManager { } } + protected void configureSshDaemon() { + int port = settings.getInteger(Keys.git.sshPort, 0); + String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); + if (port > 0) { + try { + sshDaemon = new SshDaemon(gitblit); + sshDaemon.start(); + } catch (IOException e) { + sshDaemon = null; + logger.error(MessageFormat.format("Failed to start SSH daemon on {0}:{1,number,0}", bindInterface, port), e); + } + } + } + protected void configureFanout() { // startup Fanout PubSub service if (settings.getInteger(Keys.fanout.port, 0) > 0) { diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java new file mode 100644 index 00000000..e4741ed0 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; + +/** + * + * @author Eric Myrhe + * + */ +abstract class AbstractSshCommand implements Command, SessionAware { + + protected InputStream in; + + protected OutputStream out; + + protected OutputStream err; + + protected ExitCallback exit; + + protected ServerSession session; + + @Override + public void setInputStream(InputStream in) { + this.in = in; + } + + @Override + public void setOutputStream(OutputStream out) { + this.out = out; + } + + @Override + public void setErrorStream(OutputStream err) { + this.err = err; + } + + @Override + public void setExitCallback(ExitCallback exit) { + this.exit = exit; + } + + @Override + public void setSession(final ServerSession session) { + this.session = session; + } + + @Override + public void destroy() {} + + @Override + public abstract void start(Environment env) throws IOException; +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java new file mode 100644 index 00000000..c0b4930d --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -0,0 +1,164 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.IOException; +import java.util.Scanner; + +import org.apache.sshd.server.Command; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.Environment; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PacketLineOut; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; + +import com.gitblit.git.RepositoryResolver; + +/** + * + * @author Eric Myhre + * + */ +public class SshCommandFactory implements CommandFactory { + public SshCommandFactory(RepositoryResolver repositoryResolver, UploadPackFactory uploadPackFactory, ReceivePackFactory receivePackFactory) { + this.repositoryResolver = repositoryResolver; + this.uploadPackFactory = uploadPackFactory; + this.receivePackFactory = receivePackFactory; + } + + private RepositoryResolver repositoryResolver; + + private UploadPackFactory uploadPackFactory; + + private ReceivePackFactory receivePackFactory; + + @Override + public Command createCommand(final String commandLine) { + Scanner commandScanner = new Scanner(commandLine); + final String command = commandScanner.next(); + final String argument = commandScanner.nextLine(); + + if ("git-upload-pack".equals(command)) + return new UploadPackCommand(argument); + if ("git-receive-pack".equals(command)) + return new ReceivePackCommand(argument); + return new NonCommand(); + } + + public abstract class RepositoryCommand extends AbstractSshCommand { + protected final String repositoryName; + + public RepositoryCommand(String repositoryName) { + this.repositoryName = repositoryName; + } + + @Override + public void start(Environment env) throws IOException { + Repository db = null; + try { + SshDaemonClient client = session.getAttribute(SshDaemonClient.ATTR_KEY); + db = selectRepository(client, repositoryName); + if (db == null) return; + run(client, db); + exit.onExit(0); + } catch (ServiceNotEnabledException e) { + // Ignored. Client cannot use this repository. + } catch (ServiceNotAuthorizedException e) { + // Ignored. Client cannot use this repository. + } finally { + if (db != null) + db.close(); + exit.onExit(1); + } + } + + protected Repository selectRepository(SshDaemonClient client, String name) throws IOException { + try { + return openRepository(client, name); + } catch (ServiceMayNotContinueException e) { + // An error when opening the repo means the client is expecting a ref + // advertisement, so use that style of error. + PacketLineOut pktOut = new PacketLineOut(out); + pktOut.writeString("ERR " + e.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + return null; + } + } + + protected Repository openRepository(SshDaemonClient client, String name) + throws ServiceMayNotContinueException { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + name = name.replace('\\', '/'); + + // ssh://git@thishost/path should always be name="/path" here + // + if (!name.startsWith("/")) //$NON-NLS-1$ + return null; + + try { + return repositoryResolver.open(client, name.substring(1)); + } catch (RepositoryNotFoundException e) { + // null signals it "wasn't found", which is all that is suitable + // for the remote client to know. + return null; + } catch (ServiceNotEnabledException e) { + // null signals it "wasn't found", which is all that is suitable + // for the remote client to know. + return null; + } + } + + protected abstract void run(SshDaemonClient client, Repository db) + throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException; + } + + public class UploadPackCommand extends RepositoryCommand { + public UploadPackCommand(String repositoryName) { super(repositoryName); } + + @Override + protected void run(SshDaemonClient client, Repository db) + throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { + UploadPack up = uploadPackFactory.create(client, db); + up.upload(in, out, null); + } + } + + public class ReceivePackCommand extends RepositoryCommand { + public ReceivePackCommand(String repositoryName) { super(repositoryName); } + + @Override + protected void run(SshDaemonClient client, Repository db) + throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { + ReceivePack rp = receivePackFactory.create(client, db); + rp.receive(in, out, null); + } + } + + public static class NonCommand extends AbstractSshCommand { + @Override + public void start(Environment env) { + exit.onExit(127); + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java new file mode 100644 index 00000000..26e3d67e --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java @@ -0,0 +1,217 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.mina.core.future.IoFuture; +import org.apache.mina.core.future.IoFutureListener; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.transport.socket.SocketSessionConfig; +import org.apache.sshd.SshServer; +import org.apache.sshd.common.Channel; +import org.apache.sshd.common.Cipher; +import org.apache.sshd.common.Compression; +import org.apache.sshd.common.KeyExchange; +import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.Mac; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.Signature; +import org.apache.sshd.common.cipher.AES128CBC; +import org.apache.sshd.common.cipher.AES192CBC; +import org.apache.sshd.common.cipher.AES256CBC; +import org.apache.sshd.common.cipher.BlowfishCBC; +import org.apache.sshd.common.cipher.TripleDESCBC; +import org.apache.sshd.common.compression.CompressionNone; +import org.apache.sshd.common.mac.HMACMD5; +import org.apache.sshd.common.mac.HMACMD596; +import org.apache.sshd.common.mac.HMACSHA1; +import org.apache.sshd.common.mac.HMACSHA196; +import org.apache.sshd.common.random.BouncyCastleRandom; +import org.apache.sshd.common.random.SingletonRandomFactory; +import org.apache.sshd.common.signature.SignatureDSA; +import org.apache.sshd.common.signature.SignatureRSA; +import org.apache.sshd.common.util.SecurityUtils; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.FileSystemFactory; +import org.apache.sshd.server.FileSystemView; +import org.apache.sshd.server.ForwardingFilter; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.SshFile; +import org.apache.sshd.server.UserAuth; +import org.apache.sshd.server.auth.UserAuthPublicKey; +import org.apache.sshd.server.channel.ChannelDirectTcpip; +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.kex.DHG1; +import org.apache.sshd.server.kex.DHG14; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.session.SessionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author Eric Myhre + * + */ +public class SshCommandServer extends SshServer { + + private static final Logger log = LoggerFactory.getLogger(SshCommandServer.class); + + public SshCommandServer() { + setSessionFactory(new SessionFactory() { + @Override + protected ServerSession createSession(final IoSession io) throws Exception { + log.info("connection accepted on " + io); + + if (io.getConfig() instanceof SocketSessionConfig) { + final SocketSessionConfig c = (SocketSessionConfig) io.getConfig(); + c.setKeepAlive(true); + } + + final ServerSession s = (ServerSession) super.createSession(io); + s.setAttribute(SshDaemonClient.ATTR_KEY, new SshDaemonClient()); + + io.getCloseFuture().addListener(new IoFutureListener() { + @Override + public void operationComplete(IoFuture future) { + log.info("connection closed on " + io); + } + }); + return s; + } + }); + } + + /** + * Performs most of default configuration (setup random sources, setup ciphers, + * etc; also, support for forwarding and filesystem is explicitly disallowed). + * + * {@link #setKeyPairProvider(KeyPairProvider)} and + * {@link #setPublickeyAuthenticator(PublickeyAuthenticator)} are left for you. + * And applying {@link #setCommandFactory(CommandFactory)} is probably wise if you + * want something to actually happen when users do successfully authenticate. + */ + @SuppressWarnings("unchecked") + public void setup() { + if (!SecurityUtils.isBouncyCastleRegistered()) + throw new RuntimeException("BC crypto not available"); + + setKeyExchangeFactories(Arrays.>asList( + new DHG14.Factory(), + new DHG1.Factory()) + ); + + setRandomFactory(new SingletonRandomFactory(new BouncyCastleRandom.Factory())); + + setupCiphers(); + + setCompressionFactories(Arrays.>asList( + new CompressionNone.Factory()) + ); + + setMacFactories(Arrays.>asList( + new HMACMD5.Factory(), + new HMACSHA1.Factory(), + new HMACMD596.Factory(), + new HMACSHA196.Factory()) + ); + + setChannelFactories(Arrays.>asList( + new ChannelSession.Factory(), + new ChannelDirectTcpip.Factory()) + ); + + setSignatureFactories(Arrays.>asList( + new SignatureDSA.Factory(), + new SignatureRSA.Factory()) + ); + + setFileSystemFactory(new FileSystemFactory() { + @Override + public FileSystemView createFileSystemView(Session session) throws IOException { + return new FileSystemView() { + @Override + public SshFile getFile(SshFile baseDir, String file) { + return null; + } + + @Override + public SshFile getFile(String file) { + return null; + } + }; + } + }); + + setForwardingFilter(new ForwardingFilter() { + @Override + public boolean canForwardAgent(ServerSession session) { + return false; + } + + @Override + public boolean canForwardX11(ServerSession session) { + return false; + } + + @Override + public boolean canConnect(InetSocketAddress address, ServerSession session) { + return false; + } + + @Override + public boolean canListen(InetSocketAddress address, ServerSession session) { + return false; + } + }); + + setUserAuthFactories(Arrays.>asList( + new UserAuthPublicKey.Factory()) + ); + } + + protected void setupCiphers() { + List> avail = new LinkedList>(); + avail.add(new AES128CBC.Factory()); + avail.add(new TripleDESCBC.Factory()); + avail.add(new BlowfishCBC.Factory()); + avail.add(new AES192CBC.Factory()); + avail.add(new AES256CBC.Factory()); + + for (Iterator> i = avail.iterator(); i.hasNext();) { + final NamedFactory f = i.next(); + try { + final Cipher c = f.create(); + final byte[] key = new byte[c.getBlockSize()]; + final byte[] iv = new byte[c.getIVSize()]; + c.init(Cipher.Mode.Encrypt, key, iv); + } catch (InvalidKeyException e) { + i.remove(); + } catch (Exception e) { + i.remove(); + } + } + setCipherFactories(avail); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java new file mode 100644 index 00000000..6f5d5f9e --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -0,0 +1,159 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.text.MessageFormat; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.manager.IGitblit; +import com.gitblit.utils.StringUtils; + +/** + * Manager for the ssh transport. Roughly analogous to the + * {@link com.gitblit.git.GitDaemon} class. + * + * @author Eric Myhre + * + */ +public class SshDaemon { + + private final Logger logger = LoggerFactory.getLogger(SshDaemon.class); + + /** + * 22: IANA assigned port number for ssh. Note that this is a distinct concept + * from gitblit's default conf for ssh port -- this "default" is what the git + * protocol itself defaults to if it sees and ssh url without a port. + */ + public static final int DEFAULT_PORT = 22; + + private static final String HOST_KEY_STORE = "sshKeyStore.pem"; + + private InetSocketAddress myAddress; + + private AtomicBoolean run; + + private SshCommandServer sshd; + + private RepositoryResolver repositoryResolver; + + private UploadPackFactory uploadPackFactory; + + private ReceivePackFactory receivePackFactory; + + /** + * Construct the Gitblit SSH daemon. + * + * @param gitblit + */ + public SshDaemon(IGitblit gitblit) { + + IStoredSettings settings = gitblit.getSettings(); + int port = settings.getInteger(Keys.git.sshPort, 0); + String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); + + if (StringUtils.isEmpty(bindInterface)) { + myAddress = new InetSocketAddress(port); + } else { + myAddress = new InetSocketAddress(bindInterface, port); + } + + sshd = new SshCommandServer(); + sshd.setPort(myAddress.getPort()); + sshd.setHost(myAddress.getHostName()); + sshd.setup(); + sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File(gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); + sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); + + run = new AtomicBoolean(false); + repositoryResolver = new RepositoryResolver(gitblit); + uploadPackFactory = new GitblitUploadPackFactory(gitblit); + receivePackFactory = new GitblitReceivePackFactory(gitblit); + + sshd.setCommandFactory(new SshCommandFactory( + repositoryResolver, + uploadPackFactory, + receivePackFactory + )); + } + + public int getPort() { + return myAddress.getPort(); + } + + public String formatUrl(String gituser, String servername, String repository) { + if (getPort() == DEFAULT_PORT) { + // standard port + return MessageFormat.format("{0}@{1}/{2}", gituser, servername, repository); + } else { + // non-standard port + return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", gituser, servername, getPort(), repository); + } + } + + /** + * Start this daemon on a background thread. + * + * @throws IOException + * the server socket could not be opened. + * @throws IllegalStateException + * the daemon is already running. + */ + public synchronized void start() throws IOException { + if (run.get()) { + throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); + } + + sshd.start(); + run.set(true); + + logger.info(MessageFormat.format("SSH Daemon is listening on {0}:{1,number,0}", + myAddress.getAddress().getHostAddress(), myAddress.getPort())); + } + + /** @return true if this daemon is receiving connections. */ + public boolean isRunning() { + return run.get(); + } + + /** Stop this daemon. */ + public synchronized void stop() { + if (run.get()) { + logger.info("SSH Daemon stopping..."); + run.set(false); + + try { + sshd.stop(); + } catch (InterruptedException e) { + logger.error("SSH Daemon stop interrupted", e); + } + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java new file mode 100644 index 00000000..2e8008ac --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.net.InetAddress; + +import org.apache.sshd.common.Session.AttributeKey; + +/** + * + * @author Eric Myrhe + * + */ +public class SshDaemonClient { + public static final AttributeKey ATTR_KEY = new AttributeKey(); + + public InetAddress getRemoteAddress() { + return null; + } + + public String getRemoteUser() { + return null; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java new file mode 100644 index 00000000..4c97c58d --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; + +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; + +import com.gitblit.manager.IGitblit; + +/** + * + * @author Eric Myrhe + * + */ +public class SshKeyAuthenticator implements PublickeyAuthenticator { + + protected final IGitblit gitblit; + + public SshKeyAuthenticator(IGitblit gitblit) { + this.gitblit = gitblit; + } + + @Override + public boolean authenticate(String username, PublicKey key, ServerSession session) { + // TODO actually authenticate + return true; + } +} -- cgit v1.2.3 From 7613df52959b6e2ac1094d2263be310fb3e2723b Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Mon, 17 Feb 2014 21:56:36 +0100 Subject: SSHD: Add support for generic commands Change-Id: I5a60710323ca674d70e34f7451422ec167105429 --- .../gitblit/transport/ssh/AbstractSshCommand.java | 11 +- .../gitblit/transport/ssh/CommandDispatcher.java | 44 +++ .../com/gitblit/transport/ssh/CommandMetaData.java | 31 ++ .../gitblit/transport/ssh/SshCommandFactory.java | 255 +++++++++++- .../gitblit/transport/ssh/SshCommandServer.java | 12 +- .../java/com/gitblit/transport/ssh/SshDaemon.java | 80 +++- .../com/gitblit/transport/ssh/SshDaemonClient.java | 37 -- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 116 +++++- .../gitblit/transport/ssh/SshKeyCacheEntry.java | 26 ++ .../java/com/gitblit/transport/ssh/SshSession.java | 102 +++++ .../transport/ssh/commands/BaseCommand.java | 430 ++++++++++++++++++++ .../transport/ssh/commands/CreateRepository.java | 36 ++ .../transport/ssh/commands/DispatchCommand.java | 156 ++++++++ .../gitblit/transport/ssh/commands/SshCommand.java | 45 +++ .../transport/ssh/commands/VersionCommand.java | 35 ++ src/main/java/com/gitblit/utils/IdGenerator.java | 91 +++++ .../java/com/gitblit/utils/TaskInfoFactory.java | 19 + src/main/java/com/gitblit/utils/WorkQueue.java | 340 ++++++++++++++++ .../java/com/gitblit/utils/cli/CmdLineParser.java | 440 +++++++++++++++++++++ .../com/gitblit/utils/cli/SubcommandHandler.java | 43 ++ src/main/java/log4j.properties | 1 + 21 files changed, 2260 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java create mode 100644 src/main/java/com/gitblit/transport/ssh/CommandMetaData.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshSession.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java create mode 100644 src/main/java/com/gitblit/utils/IdGenerator.java create mode 100644 src/main/java/com/gitblit/utils/TaskInfoFactory.java create mode 100644 src/main/java/com/gitblit/utils/WorkQueue.java create mode 100644 src/main/java/com/gitblit/utils/cli/CmdLineParser.java create mode 100644 src/main/java/com/gitblit/utils/cli/SubcommandHandler.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java index e4741ed0..a6681f5c 100644 --- a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java @@ -15,9 +15,12 @@ */ package com.gitblit.transport.ssh; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; @@ -25,12 +28,14 @@ import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.SessionAware; import org.apache.sshd.server.session.ServerSession; +import com.google.common.base.Charsets; + /** * * @author Eric Myrhe * */ -abstract class AbstractSshCommand implements Command, SessionAware { +public abstract class AbstractSshCommand implements Command, SessionAware { protected InputStream in; @@ -70,6 +75,10 @@ abstract class AbstractSshCommand implements Command, SessionAware { @Override public void destroy() {} + protected static PrintWriter toPrintWriter(final OutputStream o) { + return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8))); + } + @Override public abstract void start(Environment env) throws IOException; } diff --git a/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java b/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java new file mode 100644 index 00000000..18c1c331 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java @@ -0,0 +1,44 @@ +package com.gitblit.transport.ssh; + +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +import org.apache.sshd.server.Command; + +import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +public class CommandDispatcher extends DispatchCommand { + + Provider repo; + Provider version; + + @Inject + public CommandDispatcher(final @Named("create-repository") Provider repo, + final @Named("version") Provider version) { + this.repo = repo; + this.version = version; + } + + public DispatchCommand get() { + DispatchCommand root = new DispatchCommand(); + Map> origin = Maps.newHashMapWithExpectedSize(2); + origin.put("gitblit", new Provider() { + @Override + public Command get() { + Set> gitblit = Sets.newHashSetWithExpectedSize(2); + gitblit.add(repo); + gitblit.add(version); + Command cmd = new DispatchCommand(gitblit); + return cmd; + } + }); + root.setMap(origin); + return root; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java new file mode 100644 index 00000000..52231b3b --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java @@ -0,0 +1,31 @@ +//Copyright (C) 2013 The Android Open Source Project +// +//Licensed 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 com.gitblit.transport.ssh; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** +* Annotation tagged on a concrete Command to describe what it is doing +*/ +@Target({ElementType.TYPE}) +@Retention(RUNTIME) +public @interface CommandMetaData { +String name(); +String description() default ""; +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index c0b4930d..85c503d4 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -16,11 +16,23 @@ package com.gitblit.transport.ssh; import java.io.IOException; -import java.util.Scanner; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import javax.inject.Inject; import org.apache.sshd.server.Command; import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.PacketLineOut; @@ -31,8 +43,13 @@ import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.utils.WorkQueue; +import com.google.common.util.concurrent.Atomics; /** * @@ -40,31 +57,233 @@ import com.gitblit.git.RepositoryResolver; * */ public class SshCommandFactory implements CommandFactory { - public SshCommandFactory(RepositoryResolver repositoryResolver, UploadPackFactory uploadPackFactory, ReceivePackFactory receivePackFactory) { + private static final Logger logger = LoggerFactory + .getLogger(SshCommandFactory.class); + private RepositoryResolver repositoryResolver; + + private UploadPackFactory uploadPackFactory; + + private ReceivePackFactory receivePackFactory; + private final ScheduledExecutorService startExecutor; + + private CommandDispatcher dispatcher; + + @Inject + public SshCommandFactory(RepositoryResolver repositoryResolver, + UploadPackFactory uploadPackFactory, + ReceivePackFactory receivePackFactory, + WorkQueue workQueue, + CommandDispatcher d) { this.repositoryResolver = repositoryResolver; this.uploadPackFactory = uploadPackFactory; this.receivePackFactory = receivePackFactory; + this.dispatcher = d; + int threads = 2;//cfg.getInt("sshd","commandStartThreads", 2); + startExecutor = workQueue.createQueue(threads, "SshCommandStart"); } - private RepositoryResolver repositoryResolver; - - private UploadPackFactory uploadPackFactory; - - private ReceivePackFactory receivePackFactory; - @Override public Command createCommand(final String commandLine) { - Scanner commandScanner = new Scanner(commandLine); - final String command = commandScanner.next(); - final String argument = commandScanner.nextLine(); - + return new Trampoline(commandLine); + /* if ("git-upload-pack".equals(command)) return new UploadPackCommand(argument); if ("git-receive-pack".equals(command)) return new ReceivePackCommand(argument); return new NonCommand(); + */ } + private class Trampoline implements Command, SessionAware { + private final String[] argv; + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback exit; + private Environment env; + private DispatchCommand cmd; + private final AtomicBoolean logged; + private final AtomicReference> task; + + Trampoline(final String cmdLine) { + argv = split(cmdLine); + logged = new AtomicBoolean(); + task = Atomics.newReference(); + } + + @Override + public void setSession(ServerSession session) { + // TODO Auto-generated method stub + } + + public void setInputStream(final InputStream in) { + this.in = in; + } + + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + public void setErrorStream(final OutputStream err) { + this.err = err; + } + + public void setExitCallback(final ExitCallback callback) { + this.exit = callback; + } + + public void start(final Environment env) throws IOException { + this.env = env; + task.set(startExecutor.submit(new Runnable() { + public void run() { + try { + onStart(); + } catch (Exception e) { + logger.warn("Cannot start command ", e); + } + } + + @Override + public String toString() { + //return "start (user " + ctx.getSession().getUsername() + ")"; + return "start (user TODO)"; + } + })); + } + + private void onStart() throws IOException { + synchronized (this) { + //final Context old = sshScope.set(ctx); + try { + cmd = dispatcher.get(); + cmd.setArguments(argv); + cmd.setInputStream(in); + cmd.setOutputStream(out); + cmd.setErrorStream(err); + cmd.setExitCallback(new ExitCallback() { + @Override + public void onExit(int rc, String exitMessage) { + exit.onExit(translateExit(rc), exitMessage); + log(rc); + } + + @Override + public void onExit(int rc) { + exit.onExit(translateExit(rc)); + log(rc); + } + }); + cmd.start(env); + } finally { + //sshScope.set(old); + } + } + } + + private int translateExit(final int rc) { + return rc; +// +// switch (rc) { +// case BaseCommand.STATUS_NOT_ADMIN: +// return 1; +// +// case BaseCommand.STATUS_CANCEL: +// return 15 /* SIGKILL */; +// +// case BaseCommand.STATUS_NOT_FOUND: +// return 127 /* POSIX not found */; +// +// default: +// return rc; +// } + + } + + private void log(final int rc) { + if (logged.compareAndSet(false, true)) { + //log.onExecute(cmd, rc); + logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc); + } + } + + @Override + public void destroy() { + Future future = task.getAndSet(null); + if (future != null) { + future.cancel(true); +// destroyExecutor.execute(new Runnable() { +// @Override +// public void run() { +// onDestroy(); +// } +// }); + } + } + + private void onDestroy() { + synchronized (this) { + if (cmd != null) { + //final Context old = sshScope.set(ctx); + try { + cmd.destroy(); + //log(BaseCommand.STATUS_CANCEL); + } finally { + //ctx = null; + cmd = null; + //sshScope.set(old); + } + } + } + } + } + + /** Split a command line into a string array. */ + static public String[] split(String commandLine) { + final List list = new ArrayList(); + boolean inquote = false; + boolean inDblQuote = false; + StringBuilder r = new StringBuilder(); + for (int ip = 0; ip < commandLine.length();) { + final char b = commandLine.charAt(ip++); + switch (b) { + case '\t': + case ' ': + if (inquote || inDblQuote) + r.append(b); + else if (r.length() > 0) { + list.add(r.toString()); + r = new StringBuilder(); + } + continue; + case '\"': + if (inquote) + r.append(b); + else + inDblQuote = !inDblQuote; + continue; + case '\'': + if (inDblQuote) + r.append(b); + else + inquote = !inquote; + continue; + case '\\': + if (inquote || ip == commandLine.length()) + r.append(b); // literal within a quote + else + r.append(commandLine.charAt(ip++)); + continue; + default: + r.append(b); + continue; + } + } + if (r.length() > 0) { + list.add(r.toString()); + } + return list.toArray(new String[list.size()]); + } + public abstract class RepositoryCommand extends AbstractSshCommand { protected final String repositoryName; @@ -76,7 +295,7 @@ public class SshCommandFactory implements CommandFactory { public void start(Environment env) throws IOException { Repository db = null; try { - SshDaemonClient client = session.getAttribute(SshDaemonClient.ATTR_KEY); + SshSession client = session.getAttribute(SshSession.KEY); db = selectRepository(client, repositoryName); if (db == null) return; run(client, db); @@ -92,7 +311,7 @@ public class SshCommandFactory implements CommandFactory { } } - protected Repository selectRepository(SshDaemonClient client, String name) throws IOException { + protected Repository selectRepository(SshSession client, String name) throws IOException { try { return openRepository(client, name); } catch (ServiceMayNotContinueException e) { @@ -104,7 +323,7 @@ public class SshCommandFactory implements CommandFactory { } } - protected Repository openRepository(SshDaemonClient client, String name) + protected Repository openRepository(SshSession client, String name) throws ServiceMayNotContinueException { // Assume any attempt to use \ was by a Windows client // and correct to the more typical / used in Git URIs. @@ -129,7 +348,7 @@ public class SshCommandFactory implements CommandFactory { } } - protected abstract void run(SshDaemonClient client, Repository db) + protected abstract void run(SshSession client, Repository db) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException; } @@ -137,7 +356,7 @@ public class SshCommandFactory implements CommandFactory { public UploadPackCommand(String repositoryName) { super(repositoryName); } @Override - protected void run(SshDaemonClient client, Repository db) + protected void run(SshSession client, Repository db) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { UploadPack up = uploadPackFactory.create(client, db); up.upload(in, out, null); @@ -148,7 +367,7 @@ public class SshCommandFactory implements CommandFactory { public ReceivePackCommand(String repositoryName) { super(repositoryName); } @Override - protected void run(SshDaemonClient client, Repository db) + protected void run(SshSession client, Repository db) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { ReceivePack rp = receivePackFactory.create(client, db); rp.receive(in, out, null); diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java index 26e3d67e..7186737f 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java @@ -17,12 +17,15 @@ package com.gitblit.transport.ssh; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.security.InvalidKeyException; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import javax.inject.Inject; + import org.apache.mina.core.future.IoFuture; import org.apache.mina.core.future.IoFutureListener; import org.apache.mina.core.session.IoSession; @@ -69,6 +72,8 @@ import org.apache.sshd.server.session.SessionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.utils.IdGenerator; + /** * * @author Eric Myhre @@ -78,7 +83,8 @@ public class SshCommandServer extends SshServer { private static final Logger log = LoggerFactory.getLogger(SshCommandServer.class); - public SshCommandServer() { + @Inject + public SshCommandServer(final IdGenerator idGenerator) { setSessionFactory(new SessionFactory() { @Override protected ServerSession createSession(final IoSession io) throws Exception { @@ -90,7 +96,9 @@ public class SshCommandServer extends SshServer { } final ServerSession s = (ServerSession) super.createSession(io); - s.setAttribute(SshDaemonClient.ATTR_KEY, new SshDaemonClient()); + SocketAddress peer = io.getRemoteAddress(); + SshSession session = new SshSession(idGenerator.next(), peer); + s.setAttribute(SshSession.KEY, session); io.getCloseFuture().addListener(new IoFutureListener() { @Override diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 6f5d5f9e..056735a1 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -21,6 +21,10 @@ import java.net.InetSocketAddress; import java.text.MessageFormat; import java.util.concurrent.atomic.AtomicBoolean; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.sshd.server.Command; import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; @@ -34,8 +38,15 @@ import com.gitblit.git.GitblitReceivePackFactory; import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; +import com.gitblit.transport.ssh.commands.CreateRepository; +import com.gitblit.transport.ssh.commands.VersionCommand; +import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; +import dagger.Module; +import dagger.ObjectGraph; +import dagger.Provides; + /** * Manager for the ssh transport. Roughly analogous to the * {@link com.gitblit.git.GitDaemon} class. @@ -62,11 +73,7 @@ public class SshDaemon { private SshCommandServer sshd; - private RepositoryResolver repositoryResolver; - - private UploadPackFactory uploadPackFactory; - - private ReceivePackFactory receivePackFactory; + private IGitblit gitblit; /** * Construct the Gitblit SSH daemon. @@ -75,6 +82,7 @@ public class SshDaemon { */ public SshDaemon(IGitblit gitblit) { + this.gitblit = gitblit; IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); @@ -85,7 +93,8 @@ public class SshDaemon { myAddress = new InetSocketAddress(bindInterface, port); } - sshd = new SshCommandServer(); + ObjectGraph graph = ObjectGraph.create(new SshModule()); + sshd = graph.get(SshCommandServer.class); sshd.setPort(myAddress.getPort()); sshd.setHost(myAddress.getHostName()); sshd.setup(); @@ -93,15 +102,8 @@ public class SshDaemon { sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); run = new AtomicBoolean(false); - repositoryResolver = new RepositoryResolver(gitblit); - uploadPackFactory = new GitblitUploadPackFactory(gitblit); - receivePackFactory = new GitblitReceivePackFactory(gitblit); - - sshd.setCommandFactory(new SshCommandFactory( - repositoryResolver, - uploadPackFactory, - receivePackFactory - )); + SshCommandFactory f = graph.get(SshCommandFactory.class); + sshd.setCommandFactory(f); } public int getPort() { @@ -156,4 +158,52 @@ public class SshDaemon { } } } + + @Module(library = true, + injects = { + IGitblit.class, + SshCommandFactory.class, + SshCommandServer.class, + }) + public class SshModule { + @Provides @Named("create-repository") Command provideCreateRepository() { + return new CreateRepository(); + } + + @Provides @Named("version") Command provideVersion() { + return new VersionCommand(); + } + +// @Provides(type=Type.SET) @Named("git") Command provideVersionCommand2() { +// return new CreateRepository(); +// } + +// @Provides @Named("git") DispatchCommand providesGitCommand() { +// return new DispatchCommand("git"); +// } + +// @Provides (type=Type.SET) Provider provideNonCommand() { +// return new SshCommandFactory.NonCommand(); +// } + + @Provides @Singleton IdGenerator provideIdGenerator() { + return new IdGenerator(); + } + + @Provides @Singleton RepositoryResolver provideRepositoryResolver() { + return new RepositoryResolver(provideGitblit()); + } + + @Provides @Singleton UploadPackFactory provideUploadPackFactory() { + return new GitblitUploadPackFactory(provideGitblit()); + } + + @Provides @Singleton ReceivePackFactory provideReceivePackFactory() { + return new GitblitReceivePackFactory(provideGitblit()); + } + + @Provides @Singleton IGitblit provideGitblit() { + return SshDaemon.this.gitblit; + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java deleted file mode 100644 index 2e8008ac..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.net.InetAddress; - -import org.apache.sshd.common.Session.AttributeKey; - -/** - * - * @author Eric Myrhe - * - */ -public class SshDaemonClient { - public static final AttributeKey ATTR_KEY = new AttributeKey(); - - public InetAddress getRemoteAddress() { - return null; - } - - public String getRemoteUser() { - return null; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index 4c97c58d..4ab20f33 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -1,26 +1,39 @@ /* * Copyright 2014 gitblit.com. * - * Licensed 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 + * Licensed 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 + * 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. + * 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 com.gitblit.transport.ssh; +import java.io.File; +import java.io.IOException; import java.security.PublicKey; +import java.util.Locale; +import java.util.concurrent.ExecutionException; +import org.apache.commons.codec.binary.Base64; +import org.apache.sshd.common.util.Buffer; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; +import org.eclipse.jgit.lib.Constants; import com.gitblit.manager.IGitblit; +import com.google.common.base.Charsets; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.cache.Weigher; +import com.google.common.io.Files; /** * @@ -29,15 +42,84 @@ import com.gitblit.manager.IGitblit; */ public class SshKeyAuthenticator implements PublickeyAuthenticator { - protected final IGitblit gitblit; + protected final IGitblit gitblit; - public SshKeyAuthenticator(IGitblit gitblit) { - this.gitblit = gitblit; - } + LoadingCache sshKeyCache = CacheBuilder + .newBuilder().maximumWeight(2 << 20).weigher(new SshKeyCacheWeigher()) + .build(new CacheLoader() { + public SshKeyCacheEntry load(String key) throws Exception { + return loadKey(key); + } - @Override - public boolean authenticate(String username, PublicKey key, ServerSession session) { - // TODO actually authenticate - return true; - } + private SshKeyCacheEntry loadKey(String key) { + try { + // TODO(davido): retrieve absolute path to public key directory: + //String dir = gitblit.getSettings().getString("public_key_dir", "data/ssh"); + String dir = "/tmp/"; + // Expect public key file name in form: in + File file = new File(dir + key + ".pub"); + String str = Files.toString(file, Charsets.ISO_8859_1); + final String[] parts = str.split(" "); + final byte[] bin = + Base64.decodeBase64(Constants.encodeASCII(parts[1])); + return new SshKeyCacheEntry(key, new Buffer(bin).getRawPublicKey()); + } catch (IOException e) { + throw new RuntimeException("Canot read public key", e); + } + } + }); + + public SshKeyAuthenticator(IGitblit gitblit) { + this.gitblit = gitblit; + } + + @Override + public boolean authenticate(String username, final PublicKey suppliedKey, + final ServerSession session) { + final SshSession sd = session.getAttribute(SshSession.KEY); + + // if (config.getBoolean("auth", "userNameToLowerCase", false)) { + username = username.toLowerCase(Locale.US); + // } + try { + // TODO: allow multiple public keys per user + SshKeyCacheEntry key = sshKeyCache.get(username); + if (key == null) { + sd.authenticationError(username, "no-matching-key"); + return false; + } + + if (key.match(suppliedKey)) { + return success(username, session, sd); + } + return false; + } catch (ExecutionException e) { + sd.authenticationError(username, "user-not-found"); + return false; + } + } + + boolean success(String username, ServerSession session, SshSession sd) { + sd.authenticationSuccess(username); + /* + * sshLog.onLogin(); + * + * GerritServerSession s = (GerritServerSession) session; + * s.addCloseSessionListener( new SshFutureListener() { + * + * @Override public void operationComplete(CloseFuture future) { final + * Context ctx = sshScope.newContext(null, sd, null); final Context old = + * sshScope.set(ctx); try { sshLog.onLogout(); } finally { + * sshScope.set(old); } } }); } + */ + return true; + } + + private static class SshKeyCacheWeigher implements + Weigher { + @Override + public int weigh(String key, SshKeyCacheEntry value) { + return key.length() + value.weigh(); + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java b/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java new file mode 100644 index 00000000..ddc48b35 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java @@ -0,0 +1,26 @@ + +package com.gitblit.transport.ssh; + +import java.security.PublicKey; + +class SshKeyCacheEntry { + private final String user; + private final PublicKey publicKey; + + SshKeyCacheEntry(String user, PublicKey publicKey) { + this.user = user; + this.publicKey = publicKey; + } + + String getUser() { + return user; + } + + boolean match(PublicKey inkey) { + return publicKey.equals(inkey); + } + + int weigh() { + return publicKey.getEncoded().length; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshSession.java b/src/main/java/com/gitblit/transport/ssh/SshSession.java new file mode 100644 index 00000000..9f18a197 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshSession.java @@ -0,0 +1,102 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import org.apache.sshd.common.Session.AttributeKey; + +/** + * + * @author Eric Myrhe + * + */ +public class SshSession { + public static final AttributeKey KEY = + new AttributeKey(); + + private final int sessionId; + private final SocketAddress remoteAddress; + private final String remoteAsString; + + private volatile String username; + private volatile String authError; + + SshSession(int sessionId, SocketAddress peer) { + this.sessionId = sessionId; + this.remoteAddress = peer; + this.remoteAsString = format(remoteAddress); + } + + public SocketAddress getRemoteAddress() { + return remoteAddress; + } + + String getRemoteAddressAsString() { + return remoteAsString; + } + + public String getRemoteUser() { + return username; + } + + /** Unique session number, assigned during connect. */ + public int getSessionId() { + return sessionId; + } + + String getUsername() { + return username; + } + + String getAuthenticationError() { + return authError; + } + + void authenticationSuccess(String user) { + username = user; + authError = null; + } + + void authenticationError(String user, String error) { + username = user; + authError = error; + } + + /** @return {@code true} if the authentication did not succeed. */ + boolean isAuthenticationError() { + return authError != null; + } + + private static String format(final SocketAddress remote) { + if (remote instanceof InetSocketAddress) { + final InetSocketAddress sa = (InetSocketAddress) remote; + + final InetAddress in = sa.getAddress(); + if (in != null) { + return in.getHostAddress(); + } + + final String hostName = sa.getHostName(); + if (hostName != null) { + return hostName; + } + } + return remote.toString(); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java new file mode 100644 index 00000000..fd73ccfd --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -0,0 +1,430 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.common.SshException; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.transport.ssh.AbstractSshCommand; +import com.gitblit.utils.IdGenerator; +import com.gitblit.utils.WorkQueue; +import com.gitblit.utils.cli.CmdLineParser; +import com.google.common.base.Charsets; +import com.google.common.util.concurrent.Atomics; + +public abstract class BaseCommand extends AbstractSshCommand { + private static final Logger log = LoggerFactory + .getLogger(BaseCommand.class); + + /** Text of the command line which lead up to invoking this instance. */ + private String commandName = ""; + + /** Unparsed command line options. */ + private String[] argv; + + /** The task, as scheduled on a worker thread. */ + private final AtomicReference> task; + + private final WorkQueue.Executor executor; + + public BaseCommand() { + task = Atomics.newReference(); + IdGenerator gen = new IdGenerator(); + WorkQueue w = new WorkQueue(gen); + this.executor = w.getDefaultQueue(); + } + + public void setInputStream(final InputStream in) { + this.in = in; + } + + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + public void setErrorStream(final OutputStream err) { + this.err = err; + } + + public void setExitCallback(final ExitCallback callback) { + this.exit = callback; + } + + protected void provideStateTo(final Command cmd) { + cmd.setInputStream(in); + cmd.setOutputStream(out); + cmd.setErrorStream(err); + cmd.setExitCallback(exit); + } + + protected String getName() { + return commandName; + } + + void setName(final String prefix) { + this.commandName = prefix; + } + + public String[] getArguments() { + return argv; + } + + public void setArguments(final String[] argv) { + this.argv = argv; + } + + /** + * Parses the command line argument, injecting parsed values into fields. + *

+ * This method must be explicitly invoked to cause a parse. + * + * @throws UnloggedFailure if the command line arguments were invalid. + * @see Option + * @see Argument + */ + protected void parseCommandLine() throws UnloggedFailure { + parseCommandLine(this); + } + + /** + * Parses the command line argument, injecting parsed values into fields. + *

+ * This method must be explicitly invoked to cause a parse. + * + * @param options object whose fields declare Option and Argument annotations + * to describe the parameters of the command. Usually {@code this}. + * @throws UnloggedFailure if the command line arguments were invalid. + * @see Option + * @see Argument + */ + protected void parseCommandLine(Object options) throws UnloggedFailure { + final CmdLineParser clp = newCmdLineParser(options); + try { + clp.parseArgument(argv); + } catch (IllegalArgumentException err) { + if (!clp.wasHelpRequestedByOption()) { + throw new UnloggedFailure(1, "fatal: " + err.getMessage()); + } + } catch (CmdLineException err) { + if (!clp.wasHelpRequestedByOption()) { + throw new UnloggedFailure(1, "fatal: " + err.getMessage()); + } + } + + if (clp.wasHelpRequestedByOption()) { + StringWriter msg = new StringWriter(); + clp.printDetailedUsage(commandName, msg); + msg.write(usage()); + throw new UnloggedFailure(1, msg.toString()); + } + } + + /** Construct a new parser for this command's received command line. */ + protected CmdLineParser newCmdLineParser(Object options) { + return new CmdLineParser(options); + } + + protected String usage() { + return ""; + } + + private final class TaskThunk implements com.gitblit.utils.WorkQueue.CancelableRunnable { + private final CommandRunnable thunk; + private final String taskName; + + private TaskThunk(final CommandRunnable thunk) { + this.thunk = thunk; + + // TODO +// StringBuilder m = new StringBuilder("foo"); +// m.append(context.getCommandLine()); +// if (userProvider.get().isIdentifiedUser()) { +// IdentifiedUser u = (IdentifiedUser) userProvider.get(); +// m.append(" (").append(u.getAccount().getUserName()).append(")"); +// } + this.taskName = "foo";//m.toString(); + } + + @Override + public void cancel() { + synchronized (this) { + //final Context old = sshScope.set(context); + try { + //onExit(/*STATUS_CANCEL*/); + } finally { + //sshScope.set(old); + } + } + } + + @Override + public void run() { + synchronized (this) { + final Thread thisThread = Thread.currentThread(); + final String thisName = thisThread.getName(); + int rc = 0; + //final Context old = sshScope.set(context); + try { + //context.started = TimeUtil.nowMs(); + thisThread.setName("SSH " + taskName); + + thunk.run(); + + out.flush(); + err.flush(); + } catch (Throwable e) { + try { + out.flush(); + } catch (Throwable e2) { + } + try { + err.flush(); + } catch (Throwable e2) { + } + rc = handleError(e); + } finally { + try { + onExit(rc); + } finally { + thisThread.setName(thisName); + } + } + } + } + + @Override + public String toString() { + return taskName; + } + } + + /** Runnable function which can throw an exception. */ + public static interface CommandRunnable { + public void run() throws Exception; + } + + + /** + * Spawn a function into its own thread. + *

+ * Typically this should be invoked within {@link Command#start(Environment)}, + * such as: + * + *

+   * startThread(new Runnable() {
+   *   public void run() {
+   *     runImp();
+   *   }
+   * });
+   * 
+ * + * @param thunk the runnable to execute on the thread, performing the + * command's logic. + */ + protected void startThread(final Runnable thunk) { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + thunk.run(); + } + }); + } + + /** + * Terminate this command and return a result code to the remote client. + *

+ * Commands should invoke this at most once. Once invoked, the command may + * lose access to request based resources as any callbacks previously + * registered with {@link RequestCleanup} will fire. + * + * @param rc exit code for the remote client. + */ + protected void onExit(final int rc) { + exit.onExit(rc); +// if (cleanup != null) { +// cleanup.run(); +// } + } + + private int handleError(final Throwable e) { + if ((e.getClass() == IOException.class + && "Pipe closed".equals(e.getMessage())) + || // + (e.getClass() == SshException.class + && "Already closed".equals(e.getMessage())) + || // + e.getClass() == InterruptedIOException.class) { + // This is sshd telling us the client just dropped off while + // we were waiting for a read or a write to complete. Either + // way its not really a fatal error. Don't log it. + // + return 127; + } + + if (e instanceof UnloggedFailure) { + } else { + final StringBuilder m = new StringBuilder(); + m.append("Internal server error"); +// if (userProvider.get().isIdentifiedUser()) { +// final IdentifiedUser u = (IdentifiedUser) userProvider.get(); +// m.append(" (user "); +// m.append(u.getAccount().getUserName()); +// m.append(" account "); +// m.append(u.getAccountId()); +// m.append(")"); +// } +// m.append(" during "); +// m.append(contextProvider.get().getCommandLine()); + log.error(m.toString(), e); + } + + if (e instanceof Failure) { + final Failure f = (Failure) e; + try { + err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8)); + err.flush(); + } catch (IOException e2) { + } catch (Throwable e2) { + log.warn("Cannot send failure message to client", e2); + } + return f.exitCode; + + } else { + try { + err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8)); + err.flush(); + } catch (IOException e2) { + } catch (Throwable e2) { + log.warn("Cannot send internal server error message to client", e2); + } + return 128; + } + } + + /** + * Spawn a function into its own thread. + *

+ * Typically this should be invoked within {@link Command#start(Environment)}, + * such as: + * + *

+   * startThread(new CommandRunnable() {
+   *   public void run() throws Exception {
+   *     runImp();
+   *   }
+   * });
+   * 
+ *

+ * If the function throws an exception, it is translated to a simple message + * for the client, a non-zero exit code, and the stack trace is logged. + * + * @param thunk the runnable to execute on the thread, performing the + * command's logic. + */ + protected void startThread(final CommandRunnable thunk) { + final TaskThunk tt = new TaskThunk(thunk); + task.set(executor.submit(tt)); + } + + /** Thrown from {@link CommandRunnable#run()} with client message and code. */ + public static class Failure extends Exception { + private static final long serialVersionUID = 1L; + + final int exitCode; + + /** + * Create a new failure. + * + * @param exitCode exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and 255, + * inclusive. + * @param msg message to also send to the client's stderr. + */ + public Failure(final int exitCode, final String msg) { + this(exitCode, msg, null); + } + + /** + * Create a new failure. + * + * @param exitCode exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and 255, + * inclusive. + * @param msg message to also send to the client's stderr. + * @param why stack trace to include in the server's log, but is not sent to + * the client's stderr. + */ + public Failure(final int exitCode, final String msg, final Throwable why) { + super(msg, why); + this.exitCode = exitCode; + } + } + + /** Thrown from {@link CommandRunnable#run()} with client message and code. */ + public static class UnloggedFailure extends Failure { + private static final long serialVersionUID = 1L; + + /** + * Create a new failure. + * + * @param msg message to also send to the client's stderr. + */ + public UnloggedFailure(final String msg) { + this(1, msg); + } + + /** + * Create a new failure. + * + * @param exitCode exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and 255, + * inclusive. + * @param msg message to also send to the client's stderr. + */ + public UnloggedFailure(final int exitCode, final String msg) { + this(exitCode, msg, null); + } + + /** + * Create a new failure. + * + * @param exitCode exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and 255, + * inclusive. + * @param msg message to also send to the client's stderr. + * @param why stack trace to include in the server's log, but is not sent to + * the client's stderr. + */ + public UnloggedFailure(final int exitCode, final String msg, + final Throwable why) { + super(exitCode, msg, why); + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java new file mode 100644 index 00000000..802905f2 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import org.kohsuke.args4j.Option; + +import com.gitblit.transport.ssh.CommandMetaData; + +@CommandMetaData(name = "create-repository", description = "Create new GIT repository") +public class CreateRepository extends SshCommand { + + @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created") + private String name; + + @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of repository") + private String repositoryDescription; + + @Override + public void run() { + stdout.println(String.format("Repository <%s> was created", name)); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java new file mode 100644 index 00000000..672f0245 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -0,0 +1,156 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.inject.Provider; + +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.kohsuke.args4j.Argument; + +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.utils.cli.SubcommandHandler; +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + +public class DispatchCommand extends BaseCommand { + + @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class) + private String commandName; + + @Argument(index = 1, multiValued = true, metaVar = "ARG") + private List args = new ArrayList(); + + private Set> commands; + private Map> map; + + public DispatchCommand() {} + + public DispatchCommand(Map> map) { + this.map = map; + } + + public void setMap(Map> m) { + map = m; + } + + public DispatchCommand(Set> commands) { + this.commands = commands; + } + + private Map> getMap() { + if (map == null) { + map = Maps.newHashMapWithExpectedSize(commands.size()); + for (Provider cmd : commands) { + CommandMetaData meta = cmd.get().getClass().getAnnotation(CommandMetaData.class); + map.put(meta.name(), cmd); + } + } + return map; + } + + @Override + public void start(Environment env) throws IOException { + try { + parseCommandLine(); + if (Strings.isNullOrEmpty(commandName)) { + StringWriter msg = new StringWriter(); + msg.write(usage()); + throw new UnloggedFailure(1, msg.toString()); + } + + final Provider p = getMap().get(commandName); + if (p == null) { + String msg = + (getName().isEmpty() ? "Gitblit" : getName()) + ": " + + commandName + ": not found"; + throw new UnloggedFailure(1, msg); + } + + final Command cmd = p.get(); + if (cmd instanceof BaseCommand) { + BaseCommand bc = (BaseCommand) cmd; + if (getName().isEmpty()) { + bc.setName(commandName); + } else { + bc.setName(getName() + " " + commandName); + } + bc.setArguments(args.toArray(new String[args.size()])); + } else if (!args.isEmpty()) { + throw new UnloggedFailure(1, commandName + " does not take arguments"); + } + + provideStateTo(cmd); + //atomicCmd.set(cmd); + cmd.start(env); + + } catch (UnloggedFailure e) { + String msg = e.getMessage(); + if (!msg.endsWith("\n")) { + msg += "\n"; + } + err.write(msg.getBytes(Charsets.UTF_8)); + err.flush(); + exit.onExit(e.exitCode); + } + } + + protected String usage() { + final StringBuilder usage = new StringBuilder(); + usage.append("Available commands"); + if (!getName().isEmpty()) { + usage.append(" of "); + usage.append(getName()); + } + usage.append(" are:\n"); + usage.append("\n"); + + int maxLength = -1; + Map> m = getMap(); + for (String name : m.keySet()) { + maxLength = Math.max(maxLength, name.length()); + } + String format = "%-" + maxLength + "s %s"; + for (String name : Sets.newTreeSet(m.keySet())) { + final Provider p = m.get(name); + usage.append(" "); + CommandMetaData meta = p.get().getClass().getAnnotation(CommandMetaData.class); + if (meta != null) { + usage.append(String.format(format, name, + Strings.nullToEmpty(meta.description()))); + } + usage.append("\n"); + } + usage.append("\n"); + + usage.append("See '"); + if (getName().indexOf(' ') < 0) { + usage.append(getName()); + usage.append(' '); + } + usage.append("COMMAND --help' for more information.\n"); + usage.append("\n"); + return usage.toString(); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java new file mode 100644 index 00000000..44618f3b --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java @@ -0,0 +1,45 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; +import java.io.PrintWriter; + +import org.apache.sshd.server.Environment; + +public abstract class SshCommand extends BaseCommand { + protected PrintWriter stdout; + protected PrintWriter stderr; + + @Override + public void start(Environment env) throws IOException { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + stdout = toPrintWriter(out); + stderr = toPrintWriter(err); + try { + SshCommand.this.run(); + } finally { + stdout.flush(); + stderr.flush(); + } + } + }); + } + + protected abstract void run() throws UnloggedFailure, Failure, Exception; +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java new file mode 100644 index 00000000..baae6a2c --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import org.kohsuke.args4j.Option; + +import com.gitblit.Constants; +import com.gitblit.transport.ssh.CommandMetaData; + +@CommandMetaData(name="version", description = "Print Gitblit version") +public class VersionCommand extends SshCommand { + + @Option(name = "--verbose", aliases = {"-v"}, metaVar = "VERBOSE", usage = "Print verbose versions") + private boolean verbose; + + @Override + public void run() { + stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(), + verbose)); + } +} diff --git a/src/main/java/com/gitblit/utils/IdGenerator.java b/src/main/java/com/gitblit/utils/IdGenerator.java new file mode 100644 index 00000000..d2c1cb23 --- /dev/null +++ b/src/main/java/com/gitblit/utils/IdGenerator.java @@ -0,0 +1,91 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// Licensed 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 com.gitblit.utils; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +/** Simple class to produce 4 billion keys randomly distributed. */ +public class IdGenerator { + /** Format an id created by this class as a hex string. */ + public static String format(int id) { + final char[] r = new char[8]; + for (int p = 7; 0 <= p; p--) { + final int h = id & 0xf; + r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10)); + id >>= 4; + } + return new String(r); + } + + private final AtomicInteger gen; + + @Inject + public IdGenerator() { + gen = new AtomicInteger(new Random().nextInt()); + } + + /** Produce the next identifier. */ + public int next() { + return mix(gen.getAndIncrement()); + } + + private static final int salt = 0x9e3779b9; + + static int mix(int in) { + return mix(salt, in); + } + + /** A very simple bit permutation to mask a simple incrementer. */ + public static int mix(final int salt, final int in) { + short v0 = hi16(in); + short v1 = lo16(in); + v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; + v1 += ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; + return result(v0, v1); + } + + /* For testing only. */ + static int unmix(final int in) { + short v0 = hi16(in); + short v1 = lo16(in); + v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; + v0 -= ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; + return result(v0, v1); + } + + private static short hi16(final int in) { + return (short) ( // + ((in >>> 24 & 0xff)) | // + ((in >>> 16 & 0xff) << 8) // + ); + } + + private static short lo16(final int in) { + return (short) ( // + ((in >>> 8 & 0xff)) | // + ((in & 0xff) << 8) // + ); + } + + private static int result(final short v0, final short v1) { + return ((v0 & 0xff) << 24) | // + (((v0 >>> 8) & 0xff) << 16) | // + ((v1 & 0xff) << 8) | // + ((v1 >>> 8) & 0xff); + } +} diff --git a/src/main/java/com/gitblit/utils/TaskInfoFactory.java b/src/main/java/com/gitblit/utils/TaskInfoFactory.java new file mode 100644 index 00000000..111af27b --- /dev/null +++ b/src/main/java/com/gitblit/utils/TaskInfoFactory.java @@ -0,0 +1,19 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed 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 com.gitblit.utils; + +public interface TaskInfoFactory { + T getTaskInfo(WorkQueue.Task task); +} diff --git a/src/main/java/com/gitblit/utils/WorkQueue.java b/src/main/java/com/gitblit/utils/WorkQueue.java new file mode 100644 index 00000000..778e754c --- /dev/null +++ b/src/main/java/com/gitblit/utils/WorkQueue.java @@ -0,0 +1,340 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// Licensed 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 com.gitblit.utils; + +import com.google.common.collect.Lists; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.inject.Inject; + +/** Delayed execution of tasks using a background thread pool. */ +public class WorkQueue { + private static final Logger log = LoggerFactory.getLogger(WorkQueue.class); + private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION = + new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + log.error("WorkQueue thread " + t.getName() + " threw exception", e); + } + }; + + private Executor defaultQueue; + private final IdGenerator idGenerator; + private final CopyOnWriteArrayList queues; + + @Inject + public WorkQueue(final IdGenerator idGenerator) { + this.idGenerator = idGenerator; + this.queues = new CopyOnWriteArrayList(); + } + + /** Get the default work queue, for miscellaneous tasks. */ + public synchronized Executor getDefaultQueue() { + if (defaultQueue == null) { + defaultQueue = createQueue(1, "WorkQueue"); + } + return defaultQueue; + } + + /** Create a new executor queue with one thread. */ + public Executor createQueue(final int poolsize, final String prefix) { + final Executor r = new Executor(poolsize, prefix); + r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + queues.add(r); + return r; + } + + /** Get all of the tasks currently scheduled in any work queue. */ + public List> getTasks() { + final List> r = new ArrayList>(); + for (final Executor e : queues) { + e.addAllTo(r); + } + return r; + } + + public List getTaskInfos(TaskInfoFactory factory) { + List taskInfos = Lists.newArrayList(); + for (Executor exe : queues) { + for (Task task : exe.getTasks()) { + taskInfos.add(factory.getTaskInfo(task)); + } + } + return taskInfos; + } + + /** Locate a task by its unique id, null if no task matches. */ + public Task getTask(final int id) { + Task result = null; + for (final Executor e : queues) { + final Task t = e.getTask(id); + if (t != null) { + if (result != null) { + // Don't return the task if we have a duplicate. Lie instead. + return null; + } else { + result = t; + } + } + } + return result; + } + + public void stop() { + for (final Executor p : queues) { + p.shutdown(); + boolean isTerminated; + do { + try { + isTerminated = p.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException ie) { + isTerminated = false; + } + } while (!isTerminated); + } + queues.clear(); + } + + /** An isolated queue. */ + public class Executor extends ScheduledThreadPoolExecutor { + private final ConcurrentHashMap> all; + + Executor(final int corePoolSize, final String prefix) { + super(corePoolSize, new ThreadFactory() { + private final ThreadFactory parent = Executors.defaultThreadFactory(); + private final AtomicInteger tid = new AtomicInteger(1); + + @Override + public Thread newThread(final Runnable task) { + final Thread t = parent.newThread(task); + t.setName(prefix + "-" + tid.getAndIncrement()); + t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION); + return t; + } + }); + + all = new ConcurrentHashMap>( // + corePoolSize << 1, // table size + 0.75f, // load factor + corePoolSize + 4 // concurrency level + ); + } + + public void unregisterWorkQueue() { + queues.remove(this); + } + + @Override + protected RunnableScheduledFuture decorateTask( + final Runnable runnable, RunnableScheduledFuture r) { + r = super.decorateTask(runnable, r); + for (;;) { + final int id = idGenerator.next(); + + Task task; + task = new Task(runnable, r, this, id); + + if (all.putIfAbsent(task.getTaskId(), task) == null) { + return task; + } + } + } + + @Override + protected RunnableScheduledFuture decorateTask( + final Callable callable, final RunnableScheduledFuture task) { + throw new UnsupportedOperationException("Callable not implemented"); + } + + void remove(final Task task) { + all.remove(task.getTaskId(), task); + } + + Task getTask(final int id) { + return all.get(id); + } + + void addAllTo(final List> list) { + list.addAll(all.values()); // iterator is thread safe + } + + Collection> getTasks() { + return all.values(); + } + } + + /** Runnable needing to know it was canceled. */ + public interface CancelableRunnable extends Runnable { + /** Notifies the runnable it was canceled. */ + public void cancel(); + } + + /** A wrapper around a scheduled Runnable, as maintained in the queue. */ + public static class Task implements RunnableScheduledFuture { + /** + * Summarized status of a single task. + *

+ * Tasks have the following state flow: + *

    + *
  1. {@link #SLEEPING}: if scheduled with a non-zero delay.
  2. + *
  3. {@link #READY}: waiting for an available worker thread.
  4. + *
  5. {@link #RUNNING}: actively executing on a worker thread.
  6. + *
  7. {@link #DONE}: finished executing, if not periodic.
  8. + *
+ */ + public static enum State { + // Ordered like this so ordinal matches the order we would + // prefer to see tasks sorted in: done before running, + // running before ready, ready before sleeping. + // + DONE, CANCELLED, RUNNING, READY, SLEEPING, OTHER + } + + private final Runnable runnable; + private final RunnableScheduledFuture task; + private final Executor executor; + private final int taskId; + private final AtomicBoolean running; + private final Date startTime; + + Task(Runnable runnable, RunnableScheduledFuture task, Executor executor, + int taskId) { + this.runnable = runnable; + this.task = task; + this.executor = executor; + this.taskId = taskId; + this.running = new AtomicBoolean(); + this.startTime = new Date(); + } + + public int getTaskId() { + return taskId; + } + + public State getState() { + if (isCancelled()) { + return State.CANCELLED; + } else if (isDone() && !isPeriodic()) { + return State.DONE; + } else if (running.get()) { + return State.RUNNING; + } + + final long delay = getDelay(TimeUnit.MILLISECONDS); + if (delay <= 0) { + return State.READY; + } else if (0 < delay) { + return State.SLEEPING; + } + + return State.OTHER; + } + + public Date getStartTime() { + return startTime; + } + + public boolean cancel(boolean mayInterruptIfRunning) { + if (task.cancel(mayInterruptIfRunning)) { + // Tiny abuse of running: if the task needs to know it was + // canceled (to clean up resources) and it hasn't started + // yet the task's run method won't execute. So we tag it + // as running and allow it to clean up. This ensures we do + // not invoke cancel twice. + // + if (runnable instanceof CancelableRunnable + && running.compareAndSet(false, true)) { + ((CancelableRunnable) runnable).cancel(); + } + executor.remove(this); + executor.purge(); + return true; + + } else { + return false; + } + } + + public int compareTo(Delayed o) { + return task.compareTo(o); + } + + public V get() throws InterruptedException, ExecutionException { + return task.get(); + } + + public V get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return task.get(timeout, unit); + } + + public long getDelay(TimeUnit unit) { + return task.getDelay(unit); + } + + public boolean isCancelled() { + return task.isCancelled(); + } + + public boolean isDone() { + return task.isDone(); + } + + public boolean isPeriodic() { + return task.isPeriodic(); + } + + public void run() { + if (running.compareAndSet(false, true)) { + try { + task.run(); + } finally { + if (isPeriodic()) { + running.set(false); + } else { + executor.remove(this); + } + } + } + } + + @Override + public String toString() { + return runnable.toString(); + } + } +} diff --git a/src/main/java/com/gitblit/utils/cli/CmdLineParser.java b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java new file mode 100644 index 00000000..def76df4 --- /dev/null +++ b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2008, Shawn O. Pearce + * + * (Taken from JGit org.eclipse.jgit.pgm.opt.CmdLineParser.) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * - Neither the name of the Git Development Community nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +package com.gitblit.utils.cli; + +import java.io.StringWriter; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.IllegalAnnotationError; +import org.kohsuke.args4j.NamedOptionDef; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.BooleanOptionHandler; +import org.kohsuke.args4j.spi.EnumOptionHandler; +import org.kohsuke.args4j.spi.FieldSetter; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Setter; + +import com.google.common.base.Strings; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +/** + * Extended command line parser which handles --foo=value arguments. + *

+ * The args4j package does not natively handle --foo=value and instead prefers + * to see --foo value on the command line. Many users are used to the GNU style + * --foo=value long option, so we convert from the GNU style format to the + * args4j style format prior to invoking args4j for parsing. + */ +public class CmdLineParser { + public interface Factory { + CmdLineParser create(Object bean); + } + + private final MyParser parser; + + @SuppressWarnings("rawtypes") + private Map options; + + /** + * Creates a new command line owner that parses arguments/options and set them + * into the given object. + * + * @param bean instance of a class annotated by + * {@link org.kohsuke.args4j.Option} and + * {@link org.kohsuke.args4j.Argument}. this object will receive + * values. + * + * @throws IllegalAnnotationError if the option bean class is using args4j + * annotations incorrectly. + */ + public CmdLineParser(Object bean) + throws IllegalAnnotationError { + this.parser = new MyParser(bean); + } + + public void addArgument(Setter setter, Argument a) { + parser.addArgument(setter, a); + } + + public void addOption(Setter setter, Option o) { + parser.addOption(setter, o); + } + + public void printSingleLineUsage(Writer w, ResourceBundle rb) { + parser.printSingleLineUsage(w, rb); + } + + public void printUsage(Writer out, ResourceBundle rb) { + parser.printUsage(out, rb); + } + + public void printDetailedUsage(String name, StringWriter out) { + out.write(name); + printSingleLineUsage(out, null); + out.write('\n'); + out.write('\n'); + printUsage(out, null); + out.write('\n'); + } + + public void printQueryStringUsage(String name, StringWriter out) { + out.write(name); + + char next = '?'; + List booleans = new ArrayList(); + for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) { + if (handler.option instanceof NamedOptionDef) { + NamedOptionDef n = (NamedOptionDef) handler.option; + + if (handler instanceof BooleanOptionHandler) { + booleans.add(n); + continue; + } + + if (!n.required()) { + out.write('['); + } + out.write(next); + next = '&'; + if (n.name().startsWith("--")) { + out.write(n.name().substring(2)); + } else if (n.name().startsWith("-")) { + out.write(n.name().substring(1)); + } else { + out.write(n.name()); + } + out.write('='); + + out.write(metaVar(handler, n)); + if (!n.required()) { + out.write(']'); + } + if (n.isMultiValued()) { + out.write('*'); + } + } + } + for (NamedOptionDef n : booleans) { + if (!n.required()) { + out.write('['); + } + out.write(next); + next = '&'; + if (n.name().startsWith("--")) { + out.write(n.name().substring(2)); + } else if (n.name().startsWith("-")) { + out.write(n.name().substring(1)); + } else { + out.write(n.name()); + } + if (!n.required()) { + out.write(']'); + } + } + } + + private static String metaVar(OptionHandler handler, NamedOptionDef n) { + String var = n.metaVar(); + if (Strings.isNullOrEmpty(var)) { + var = handler.getDefaultMetaVariable(); + if (handler instanceof EnumOptionHandler) { + var = var.substring(1, var.length() - 1).replace(" ", ""); + } + } + return var; + } + + public boolean wasHelpRequestedByOption() { + return parser.help.value; + } + + public void parseArgument(final String... args) throws CmdLineException { + List tmp = Lists.newArrayListWithCapacity(args.length); + for (int argi = 0; argi < args.length; argi++) { + final String str = args[argi]; + if (str.equals("--")) { + while (argi < args.length) + tmp.add(args[argi++]); + break; + } + + if (str.startsWith("--")) { + final int eq = str.indexOf('='); + if (eq > 0) { + tmp.add(str.substring(0, eq)); + tmp.add(str.substring(eq + 1)); + continue; + } + } + + tmp.add(str); + } + parser.parseArgument(tmp.toArray(new String[tmp.size()])); + } + + public void parseOptionMap(Map parameters) + throws CmdLineException { + Multimap map = LinkedHashMultimap.create(); + for (Map.Entry ent : parameters.entrySet()) { + for (String val : ent.getValue()) { + map.put(ent.getKey(), val); + } + } + parseOptionMap(map); + } + + public void parseOptionMap(Multimap params) + throws CmdLineException { + List tmp = Lists.newArrayListWithCapacity(2 * params.size()); + for (final String key : params.keySet()) { + String name = makeOption(key); + + if (isBoolean(name)) { + boolean on = false; + for (String value : params.get(key)) { + on = toBoolean(key, value); + } + if (on) { + tmp.add(name); + } + } else { + for (String value : params.get(key)) { + tmp.add(name); + tmp.add(value); + } + } + } + parser.parseArgument(tmp.toArray(new String[tmp.size()])); + } + + public boolean isBoolean(String name) { + return findHandler(makeOption(name)) instanceof BooleanOptionHandler; + } + + private String makeOption(String name) { + if (!name.startsWith("-")) { + if (name.length() == 1) { + name = "-" + name; + } else { + name = "--" + name; + } + } + return name; + } + + @SuppressWarnings("rawtypes") + private OptionHandler findHandler(String name) { + if (options == null) { + options = index(parser.options); + } + return options.get(name); + } + + @SuppressWarnings("rawtypes") + private static Map index(List in) { + Map m = Maps.newHashMap(); + for (OptionHandler handler : in) { + if (handler.option instanceof NamedOptionDef) { + NamedOptionDef def = (NamedOptionDef) handler.option; + if (!def.isArgument()) { + m.put(def.name(), handler); + for (String alias : def.aliases()) { + m.put(alias, handler); + } + } + } + } + return m; + } + + private boolean toBoolean(String name, String value) throws CmdLineException { + if ("true".equals(value) || "t".equals(value) + || "yes".equals(value) || "y".equals(value) + || "on".equals(value) + || "1".equals(value) + || value == null || "".equals(value)) { + return true; + } + + if ("false".equals(value) || "f".equals(value) + || "no".equals(value) || "n".equals(value) + || "off".equals(value) + || "0".equals(value)) { + return false; + } + + throw new CmdLineException(parser, String.format( + "invalid boolean \"%s=%s\"", name, value)); + } + + private class MyParser extends org.kohsuke.args4j.CmdLineParser { + @SuppressWarnings("rawtypes") + private List options; + private HelpOption help; + + MyParser(final Object bean) { + super(bean); + ensureOptionsInitialized(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + protected OptionHandler createOptionHandler(final OptionDef option, + final Setter setter) { + if (isHandlerSpecified(option) || isEnum(setter) || isPrimitive(setter)) { + return add(super.createOptionHandler(option, setter)); + } + +// OptionHandlerFactory factory = handlers.get(setter.getType()); +// if (factory != null) { +// return factory.create(this, option, setter); +// } + return add(super.createOptionHandler(option, setter)); + } + + @SuppressWarnings("rawtypes") + private OptionHandler add(OptionHandler handler) { + ensureOptionsInitialized(); + options.add(handler); + return handler; + } + + private void ensureOptionsInitialized() { + if (options == null) { + help = new HelpOption(); + options = Lists.newArrayList(); + addOption(help, help); + } + } + + private boolean isHandlerSpecified(final OptionDef option) { + return option.handler() != OptionHandler.class; + } + + private boolean isEnum(Setter setter) { + return Enum.class.isAssignableFrom(setter.getType()); + } + + private boolean isPrimitive(Setter setter) { + return setter.getType().isPrimitive(); + } + } + + private static class HelpOption implements Option, Setter { + private boolean value; + + @Override + public String name() { + return "--help"; + } + + @Override + public String[] aliases() { + return new String[] {"-h"}; + } + + @Override + public String[] depends() { + return new String[] {}; + } + + @Override + public boolean hidden() { + return false; + } + + @Override + public String usage() { + return "display this help text"; + } + + @Override + public void addValue(Boolean val) { + value = val; + } + + @Override + public Class> handler() { + return BooleanOptionHandler.class; + } + + @Override + public String metaVar() { + return ""; + } + + @Override + public boolean required() { + return false; + } + + @Override + public Class annotationType() { + return Option.class; + } + + @Override + public FieldSetter asFieldSetter() { + throw new UnsupportedOperationException(); + } + + @Override + public AnnotatedElement asAnnotatedElement() { + throw new UnsupportedOperationException(); + } + + @Override + public Class getType() { + return Boolean.class; + } + + @Override + public boolean isMultiValued() { + return false; + } + } +} diff --git a/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java b/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java new file mode 100644 index 00000000..b1ace324 --- /dev/null +++ b/src/main/java/com/gitblit/utils/cli/SubcommandHandler.java @@ -0,0 +1,43 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// Licensed 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 com.gitblit.utils.cli; + +import org.kohsuke.args4j.CmdLineException; +import org.kohsuke.args4j.CmdLineParser; +import org.kohsuke.args4j.OptionDef; +import org.kohsuke.args4j.spi.OptionHandler; +import org.kohsuke.args4j.spi.Parameters; +import org.kohsuke.args4j.spi.Setter; + +public class SubcommandHandler extends OptionHandler { + + public SubcommandHandler(CmdLineParser parser, + OptionDef option, Setter setter) { + super(parser, option, setter); + } + + @Override + public final int parseArguments(final Parameters params) + throws CmdLineException { + setter.addValue(params.getParameter(0)); + owner.stopOptionParsing(); + return 1; + } + + @Override + public final String getDefaultMetaVariable() { + return "COMMAND"; + } +} diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties index c6b5d8c3..115dcd01 100644 --- a/src/main/java/log4j.properties +++ b/src/main/java/log4j.properties @@ -25,6 +25,7 @@ log4j.rootCategory=INFO, S #log4j.logger.net=INFO #log4j.logger.com.gitblit=DEBUG +log4j.logger.org.apache.sshd=ERROR log4j.logger.org.apache.wicket=INFO log4j.logger.org.apache.wicket.RequestListenerInterface=WARN -- cgit v1.2.3 From 9a6fe4a4d5dad241ad85677fd687aa2d3d423043 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Sat, 22 Feb 2014 17:03:37 +0100 Subject: Factor out methods from SshCommandServer to SshDaemon Change-Id: I02d545a8ab68b06d733ad8e7ed476767a34aa244 --- .../java/com/gitblit/manager/ServicesManager.java | 55 +++- .../gitblit/transport/ssh/SshCommandServer.java | 225 ---------------- .../java/com/gitblit/transport/ssh/SshDaemon.java | 288 +++++++++++++++------ 3 files changed, 259 insertions(+), 309 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshCommandServer.java (limited to 'src') diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index 2cd583ad..219e4ea5 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -24,8 +24,13 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import javax.inject.Named; +import javax.inject.Singleton; import javax.servlet.http.HttpServletRequest; +import org.apache.sshd.server.Command; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,14 +43,26 @@ import com.gitblit.fanout.FanoutNioService; import com.gitblit.fanout.FanoutService; import com.gitblit.fanout.FanoutSocketService; import com.gitblit.git.GitDaemon; +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; import com.gitblit.models.FederationModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.service.FederationPullService; +import com.gitblit.transport.ssh.SshCommandFactory; import com.gitblit.transport.ssh.SshDaemon; +import com.gitblit.transport.ssh.SshSession; +import com.gitblit.transport.ssh.commands.CreateRepository; +import com.gitblit.transport.ssh.commands.VersionCommand; +import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; import com.gitblit.utils.TimeUtils; +import dagger.Module; +import dagger.ObjectGraph; +import dagger.Provides; + /** * Services manager manages long-running services/processes that either have no * direct relation to other managers OR require really high-level manager @@ -147,7 +164,7 @@ public class ServicesManager implements IManager { String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); if (port > 0) { try { - sshDaemon = new SshDaemon(gitblit); + sshDaemon = ObjectGraph.create(new SshModule()).get(SshDaemon.class); sshDaemon.start(); } catch (IOException e) { sshDaemon = null; @@ -245,4 +262,40 @@ public class ServicesManager implements IManager { } } + + @Module(library = true, + injects = { + IGitblit.class, + SshCommandFactory.class, + SshDaemon.class, + }) + public class SshModule { + @Provides @Named("create-repository") Command provideCreateRepository() { + return new CreateRepository(); + } + + @Provides @Named("version") Command provideVersion() { + return new VersionCommand(); + } + + @Provides @Singleton IdGenerator provideIdGenerator() { + return new IdGenerator(); + } + + @Provides @Singleton RepositoryResolver provideRepositoryResolver() { + return new RepositoryResolver(provideGitblit()); + } + + @Provides @Singleton UploadPackFactory provideUploadPackFactory() { + return new GitblitUploadPackFactory(provideGitblit()); + } + + @Provides @Singleton ReceivePackFactory provideReceivePackFactory() { + return new GitblitReceivePackFactory(provideGitblit()); + } + + @Provides @Singleton IGitblit provideGitblit() { + return ServicesManager.this.gitblit; + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java b/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java deleted file mode 100644 index 7186737f..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandServer.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.security.InvalidKeyException; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import javax.inject.Inject; - -import org.apache.mina.core.future.IoFuture; -import org.apache.mina.core.future.IoFutureListener; -import org.apache.mina.core.session.IoSession; -import org.apache.mina.transport.socket.SocketSessionConfig; -import org.apache.sshd.SshServer; -import org.apache.sshd.common.Channel; -import org.apache.sshd.common.Cipher; -import org.apache.sshd.common.Compression; -import org.apache.sshd.common.KeyExchange; -import org.apache.sshd.common.KeyPairProvider; -import org.apache.sshd.common.Mac; -import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.Session; -import org.apache.sshd.common.Signature; -import org.apache.sshd.common.cipher.AES128CBC; -import org.apache.sshd.common.cipher.AES192CBC; -import org.apache.sshd.common.cipher.AES256CBC; -import org.apache.sshd.common.cipher.BlowfishCBC; -import org.apache.sshd.common.cipher.TripleDESCBC; -import org.apache.sshd.common.compression.CompressionNone; -import org.apache.sshd.common.mac.HMACMD5; -import org.apache.sshd.common.mac.HMACMD596; -import org.apache.sshd.common.mac.HMACSHA1; -import org.apache.sshd.common.mac.HMACSHA196; -import org.apache.sshd.common.random.BouncyCastleRandom; -import org.apache.sshd.common.random.SingletonRandomFactory; -import org.apache.sshd.common.signature.SignatureDSA; -import org.apache.sshd.common.signature.SignatureRSA; -import org.apache.sshd.common.util.SecurityUtils; -import org.apache.sshd.server.CommandFactory; -import org.apache.sshd.server.FileSystemFactory; -import org.apache.sshd.server.FileSystemView; -import org.apache.sshd.server.ForwardingFilter; -import org.apache.sshd.server.PublickeyAuthenticator; -import org.apache.sshd.server.SshFile; -import org.apache.sshd.server.UserAuth; -import org.apache.sshd.server.auth.UserAuthPublicKey; -import org.apache.sshd.server.channel.ChannelDirectTcpip; -import org.apache.sshd.server.channel.ChannelSession; -import org.apache.sshd.server.kex.DHG1; -import org.apache.sshd.server.kex.DHG14; -import org.apache.sshd.server.session.ServerSession; -import org.apache.sshd.server.session.SessionFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.utils.IdGenerator; - -/** - * - * @author Eric Myhre - * - */ -public class SshCommandServer extends SshServer { - - private static final Logger log = LoggerFactory.getLogger(SshCommandServer.class); - - @Inject - public SshCommandServer(final IdGenerator idGenerator) { - setSessionFactory(new SessionFactory() { - @Override - protected ServerSession createSession(final IoSession io) throws Exception { - log.info("connection accepted on " + io); - - if (io.getConfig() instanceof SocketSessionConfig) { - final SocketSessionConfig c = (SocketSessionConfig) io.getConfig(); - c.setKeepAlive(true); - } - - final ServerSession s = (ServerSession) super.createSession(io); - SocketAddress peer = io.getRemoteAddress(); - SshSession session = new SshSession(idGenerator.next(), peer); - s.setAttribute(SshSession.KEY, session); - - io.getCloseFuture().addListener(new IoFutureListener() { - @Override - public void operationComplete(IoFuture future) { - log.info("connection closed on " + io); - } - }); - return s; - } - }); - } - - /** - * Performs most of default configuration (setup random sources, setup ciphers, - * etc; also, support for forwarding and filesystem is explicitly disallowed). - * - * {@link #setKeyPairProvider(KeyPairProvider)} and - * {@link #setPublickeyAuthenticator(PublickeyAuthenticator)} are left for you. - * And applying {@link #setCommandFactory(CommandFactory)} is probably wise if you - * want something to actually happen when users do successfully authenticate. - */ - @SuppressWarnings("unchecked") - public void setup() { - if (!SecurityUtils.isBouncyCastleRegistered()) - throw new RuntimeException("BC crypto not available"); - - setKeyExchangeFactories(Arrays.>asList( - new DHG14.Factory(), - new DHG1.Factory()) - ); - - setRandomFactory(new SingletonRandomFactory(new BouncyCastleRandom.Factory())); - - setupCiphers(); - - setCompressionFactories(Arrays.>asList( - new CompressionNone.Factory()) - ); - - setMacFactories(Arrays.>asList( - new HMACMD5.Factory(), - new HMACSHA1.Factory(), - new HMACMD596.Factory(), - new HMACSHA196.Factory()) - ); - - setChannelFactories(Arrays.>asList( - new ChannelSession.Factory(), - new ChannelDirectTcpip.Factory()) - ); - - setSignatureFactories(Arrays.>asList( - new SignatureDSA.Factory(), - new SignatureRSA.Factory()) - ); - - setFileSystemFactory(new FileSystemFactory() { - @Override - public FileSystemView createFileSystemView(Session session) throws IOException { - return new FileSystemView() { - @Override - public SshFile getFile(SshFile baseDir, String file) { - return null; - } - - @Override - public SshFile getFile(String file) { - return null; - } - }; - } - }); - - setForwardingFilter(new ForwardingFilter() { - @Override - public boolean canForwardAgent(ServerSession session) { - return false; - } - - @Override - public boolean canForwardX11(ServerSession session) { - return false; - } - - @Override - public boolean canConnect(InetSocketAddress address, ServerSession session) { - return false; - } - - @Override - public boolean canListen(InetSocketAddress address, ServerSession session) { - return false; - } - }); - - setUserAuthFactories(Arrays.>asList( - new UserAuthPublicKey.Factory()) - ); - } - - protected void setupCiphers() { - List> avail = new LinkedList>(); - avail.add(new AES128CBC.Factory()); - avail.add(new TripleDESCBC.Factory()); - avail.add(new BlowfishCBC.Factory()); - avail.add(new AES192CBC.Factory()); - avail.add(new AES256CBC.Factory()); - - for (Iterator> i = avail.iterator(); i.hasNext();) { - final NamedFactory f = i.next(); - try { - final Cipher c = f.create(); - final byte[] key = new byte[c.getBlockSize()]; - final byte[] iv = new byte[c.getIVSize()]; - c.init(Cipher.Mode.Encrypt, key, iv); - } catch (InvalidKeyException e) { - i.remove(); - } catch (Exception e) { - i.remove(); - } - } - setCipherFactories(avail); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 056735a1..3c3ef3f2 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -18,35 +18,71 @@ package com.gitblit.transport.ssh; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.InvalidKeyException; import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import javax.inject.Named; -import javax.inject.Singleton; - -import org.apache.sshd.server.Command; +import javax.inject.Inject; + +import org.apache.mina.core.future.IoFuture; +import org.apache.mina.core.future.IoFutureListener; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.transport.socket.SocketSessionConfig; +import org.apache.sshd.SshServer; +import org.apache.sshd.common.Channel; +import org.apache.sshd.common.Cipher; +import org.apache.sshd.common.Compression; +import org.apache.sshd.common.KeyExchange; +import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.Mac; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.Signature; +import org.apache.sshd.common.cipher.AES128CBC; +import org.apache.sshd.common.cipher.AES192CBC; +import org.apache.sshd.common.cipher.AES256CBC; +import org.apache.sshd.common.cipher.BlowfishCBC; +import org.apache.sshd.common.cipher.TripleDESCBC; +import org.apache.sshd.common.compression.CompressionNone; +import org.apache.sshd.common.mac.HMACMD5; +import org.apache.sshd.common.mac.HMACMD596; +import org.apache.sshd.common.mac.HMACSHA1; +import org.apache.sshd.common.mac.HMACSHA196; +import org.apache.sshd.common.random.BouncyCastleRandom; +import org.apache.sshd.common.random.SingletonRandomFactory; +import org.apache.sshd.common.signature.SignatureDSA; +import org.apache.sshd.common.signature.SignatureRSA; +import org.apache.sshd.common.util.SecurityUtils; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.FileSystemFactory; +import org.apache.sshd.server.FileSystemView; +import org.apache.sshd.server.ForwardingFilter; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.SshFile; +import org.apache.sshd.server.UserAuth; +import org.apache.sshd.server.auth.UserAuthPublicKey; +import org.apache.sshd.server.channel.ChannelDirectTcpip; +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.kex.DHG1; +import org.apache.sshd.server.kex.DHG14; import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.session.SessionFactory; import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.IStoredSettings; import com.gitblit.Keys; -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; -import com.gitblit.transport.ssh.commands.CreateRepository; -import com.gitblit.transport.ssh.commands.VersionCommand; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; -import dagger.Module; -import dagger.ObjectGraph; -import dagger.Provides; - /** * Manager for the ssh transport. Roughly analogous to the * {@link com.gitblit.git.GitDaemon} class. @@ -54,9 +90,9 @@ import dagger.Provides; * @author Eric Myhre * */ -public class SshDaemon { +public class SshDaemon extends SshServer { - private final Logger logger = LoggerFactory.getLogger(SshDaemon.class); + private final Logger log = LoggerFactory.getLogger(SshDaemon.class); /** * 22: IANA assigned port number for ssh. Note that this is a distinct concept @@ -71,17 +107,16 @@ public class SshDaemon { private AtomicBoolean run; - private SshCommandServer sshd; - - private IGitblit gitblit; + @SuppressWarnings("unused") + private IGitblit gitblit; /** * Construct the Gitblit SSH daemon. * * @param gitblit */ - public SshDaemon(IGitblit gitblit) { - + @Inject + SshDaemon(IGitblit gitblit, IdGenerator idGenerator, SshCommandFactory factory) { this.gitblit = gitblit; IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); @@ -93,17 +128,40 @@ public class SshDaemon { myAddress = new InetSocketAddress(bindInterface, port); } - ObjectGraph graph = ObjectGraph.create(new SshModule()); - sshd = graph.get(SshCommandServer.class); - sshd.setPort(myAddress.getPort()); - sshd.setHost(myAddress.getHostName()); - sshd.setup(); - sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File(gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); - sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); + setPort(myAddress.getPort()); + setHost(myAddress.getHostName()); + setup(); + setKeyPairProvider(new PEMGeneratorHostKeyProvider( + new File(gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); + setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); run = new AtomicBoolean(false); - SshCommandFactory f = graph.get(SshCommandFactory.class); - sshd.setCommandFactory(f); + setCommandFactory(factory); + setSessionFactory(newSessionFactory(idGenerator)); + } + + SessionFactory newSessionFactory(final IdGenerator idGenerator) { + return new SessionFactory() { + @Override + protected ServerSession createSession(final IoSession io) throws Exception { + log.info("connection accepted on " + io); + if (io.getConfig() instanceof SocketSessionConfig) { + final SocketSessionConfig c = (SocketSessionConfig) io.getConfig(); + c.setKeepAlive(true); + } + ServerSession s = (ServerSession) super.createSession(io); + SocketAddress peer = io.getRemoteAddress(); + SshSession session = new SshSession(idGenerator.next(), peer); + s.setAttribute(SshSession.KEY, session); + io.getCloseFuture().addListener(new IoFutureListener() { + @Override + public void operationComplete(IoFuture future) { + log.info("connection closed on " + io); + } + }); + return s; + } + }; } public int getPort() { @@ -133,10 +191,10 @@ public class SshDaemon { throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); } - sshd.start(); + super.start(); run.set(true); - logger.info(MessageFormat.format("SSH Daemon is listening on {0}:{1,number,0}", + log.info(MessageFormat.format("SSH Daemon is listening on {0}:{1,number,0}", myAddress.getAddress().getHostAddress(), myAddress.getPort())); } @@ -148,62 +206,126 @@ public class SshDaemon { /** Stop this daemon. */ public synchronized void stop() { if (run.get()) { - logger.info("SSH Daemon stopping..."); + log.info("SSH Daemon stopping..."); run.set(false); try { - sshd.stop(); + super.stop(); } catch (InterruptedException e) { - logger.error("SSH Daemon stop interrupted", e); + log.error("SSH Daemon stop interrupted", e); } } } - @Module(library = true, - injects = { - IGitblit.class, - SshCommandFactory.class, - SshCommandServer.class, - }) - public class SshModule { - @Provides @Named("create-repository") Command provideCreateRepository() { - return new CreateRepository(); - } - - @Provides @Named("version") Command provideVersion() { - return new VersionCommand(); - } - -// @Provides(type=Type.SET) @Named("git") Command provideVersionCommand2() { -// return new CreateRepository(); -// } - -// @Provides @Named("git") DispatchCommand providesGitCommand() { -// return new DispatchCommand("git"); -// } - -// @Provides (type=Type.SET) Provider provideNonCommand() { -// return new SshCommandFactory.NonCommand(); -// } - - @Provides @Singleton IdGenerator provideIdGenerator() { - return new IdGenerator(); - } - - @Provides @Singleton RepositoryResolver provideRepositoryResolver() { - return new RepositoryResolver(provideGitblit()); - } - - @Provides @Singleton UploadPackFactory provideUploadPackFactory() { - return new GitblitUploadPackFactory(provideGitblit()); - } - - @Provides @Singleton ReceivePackFactory provideReceivePackFactory() { - return new GitblitReceivePackFactory(provideGitblit()); - } - - @Provides @Singleton IGitblit provideGitblit() { - return SshDaemon.this.gitblit; - } - } + /** + * Performs most of default configuration (setup random sources, setup ciphers, + * etc; also, support for forwarding and filesystem is explicitly disallowed). + * + * {@link #setKeyPairProvider(KeyPairProvider)} and + * {@link #setPublickeyAuthenticator(PublickeyAuthenticator)} are left for you. + * And applying {@link #setCommandFactory(CommandFactory)} is probably wise if you + * want something to actually happen when users do successfully authenticate. + */ + @SuppressWarnings("unchecked") + public void setup() { + if (!SecurityUtils.isBouncyCastleRegistered()) + throw new RuntimeException("BC crypto not available"); + + setKeyExchangeFactories(Arrays.>asList( + new DHG14.Factory(), + new DHG1.Factory()) + ); + + setRandomFactory(new SingletonRandomFactory(new BouncyCastleRandom.Factory())); + + setupCiphers(); + + setCompressionFactories(Arrays.>asList( + new CompressionNone.Factory()) + ); + + setMacFactories(Arrays.>asList( + new HMACMD5.Factory(), + new HMACSHA1.Factory(), + new HMACMD596.Factory(), + new HMACSHA196.Factory()) + ); + + setChannelFactories(Arrays.>asList( + new ChannelSession.Factory(), + new ChannelDirectTcpip.Factory()) + ); + + setSignatureFactories(Arrays.>asList( + new SignatureDSA.Factory(), + new SignatureRSA.Factory()) + ); + + setFileSystemFactory(new FileSystemFactory() { + @Override + public FileSystemView createFileSystemView(Session session) throws IOException { + return new FileSystemView() { + @Override + public SshFile getFile(SshFile baseDir, String file) { + return null; + } + + @Override + public SshFile getFile(String file) { + return null; + } + }; + } + }); + + setForwardingFilter(new ForwardingFilter() { + @Override + public boolean canForwardAgent(ServerSession session) { + return false; + } + + @Override + public boolean canForwardX11(ServerSession session) { + return false; + } + + @Override + public boolean canConnect(InetSocketAddress address, ServerSession session) { + return false; + } + + @Override + public boolean canListen(InetSocketAddress address, ServerSession session) { + return false; + } + }); + + setUserAuthFactories(Arrays.>asList( + new UserAuthPublicKey.Factory()) + ); + } + + protected void setupCiphers() { + List> avail = new LinkedList>(); + avail.add(new AES128CBC.Factory()); + avail.add(new TripleDESCBC.Factory()); + avail.add(new BlowfishCBC.Factory()); + avail.add(new AES192CBC.Factory()); + avail.add(new AES256CBC.Factory()); + + for (Iterator> i = avail.iterator(); i.hasNext();) { + final NamedFactory f = i.next(); + try { + final Cipher c = f.create(); + final byte[] key = new byte[c.getBlockSize()]; + final byte[] iv = new byte[c.getIVSize()]; + c.init(Cipher.Mode.Encrypt, key, iv); + } catch (InvalidKeyException e) { + i.remove(); + } catch (Exception e) { + i.remove(); + } + } + setCipherFactories(avail); + } } -- cgit v1.2.3 From af816d3fdd18d6d7d1b2c854b70eb30be789d466 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Sat, 22 Feb 2014 17:08:49 +0100 Subject: Convert SshDaemon to unix format Change-Id: Icb415c2bc62321ddd0ae08445d97f1f8102adee8 --- .../java/com/gitblit/transport/ssh/SshDaemon.java | 666 +++++++++++---------- 1 file changed, 335 insertions(+), 331 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 3c3ef3f2..b3471a29 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -1,331 +1,335 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.security.InvalidKeyException; -import java.text.MessageFormat; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.inject.Inject; - -import org.apache.mina.core.future.IoFuture; -import org.apache.mina.core.future.IoFutureListener; -import org.apache.mina.core.session.IoSession; -import org.apache.mina.transport.socket.SocketSessionConfig; -import org.apache.sshd.SshServer; -import org.apache.sshd.common.Channel; -import org.apache.sshd.common.Cipher; -import org.apache.sshd.common.Compression; -import org.apache.sshd.common.KeyExchange; -import org.apache.sshd.common.KeyPairProvider; -import org.apache.sshd.common.Mac; -import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.Session; -import org.apache.sshd.common.Signature; -import org.apache.sshd.common.cipher.AES128CBC; -import org.apache.sshd.common.cipher.AES192CBC; -import org.apache.sshd.common.cipher.AES256CBC; -import org.apache.sshd.common.cipher.BlowfishCBC; -import org.apache.sshd.common.cipher.TripleDESCBC; -import org.apache.sshd.common.compression.CompressionNone; -import org.apache.sshd.common.mac.HMACMD5; -import org.apache.sshd.common.mac.HMACMD596; -import org.apache.sshd.common.mac.HMACSHA1; -import org.apache.sshd.common.mac.HMACSHA196; -import org.apache.sshd.common.random.BouncyCastleRandom; -import org.apache.sshd.common.random.SingletonRandomFactory; -import org.apache.sshd.common.signature.SignatureDSA; -import org.apache.sshd.common.signature.SignatureRSA; -import org.apache.sshd.common.util.SecurityUtils; -import org.apache.sshd.server.CommandFactory; -import org.apache.sshd.server.FileSystemFactory; -import org.apache.sshd.server.FileSystemView; -import org.apache.sshd.server.ForwardingFilter; -import org.apache.sshd.server.PublickeyAuthenticator; -import org.apache.sshd.server.SshFile; -import org.apache.sshd.server.UserAuth; -import org.apache.sshd.server.auth.UserAuthPublicKey; -import org.apache.sshd.server.channel.ChannelDirectTcpip; -import org.apache.sshd.server.channel.ChannelSession; -import org.apache.sshd.server.kex.DHG1; -import org.apache.sshd.server.kex.DHG14; -import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; -import org.apache.sshd.server.session.ServerSession; -import org.apache.sshd.server.session.SessionFactory; -import org.eclipse.jgit.internal.JGitText; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.IStoredSettings; -import com.gitblit.Keys; -import com.gitblit.manager.IGitblit; -import com.gitblit.utils.IdGenerator; -import com.gitblit.utils.StringUtils; - -/** - * Manager for the ssh transport. Roughly analogous to the - * {@link com.gitblit.git.GitDaemon} class. - * - * @author Eric Myhre - * - */ -public class SshDaemon extends SshServer { - - private final Logger log = LoggerFactory.getLogger(SshDaemon.class); - - /** - * 22: IANA assigned port number for ssh. Note that this is a distinct concept - * from gitblit's default conf for ssh port -- this "default" is what the git - * protocol itself defaults to if it sees and ssh url without a port. - */ - public static final int DEFAULT_PORT = 22; - - private static final String HOST_KEY_STORE = "sshKeyStore.pem"; - - private InetSocketAddress myAddress; - - private AtomicBoolean run; - - @SuppressWarnings("unused") - private IGitblit gitblit; - - /** - * Construct the Gitblit SSH daemon. - * - * @param gitblit - */ - @Inject - SshDaemon(IGitblit gitblit, IdGenerator idGenerator, SshCommandFactory factory) { - this.gitblit = gitblit; - IStoredSettings settings = gitblit.getSettings(); - int port = settings.getInteger(Keys.git.sshPort, 0); - String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); - - if (StringUtils.isEmpty(bindInterface)) { - myAddress = new InetSocketAddress(port); - } else { - myAddress = new InetSocketAddress(bindInterface, port); - } - - setPort(myAddress.getPort()); - setHost(myAddress.getHostName()); - setup(); - setKeyPairProvider(new PEMGeneratorHostKeyProvider( - new File(gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); - setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); - - run = new AtomicBoolean(false); - setCommandFactory(factory); - setSessionFactory(newSessionFactory(idGenerator)); - } - - SessionFactory newSessionFactory(final IdGenerator idGenerator) { - return new SessionFactory() { - @Override - protected ServerSession createSession(final IoSession io) throws Exception { - log.info("connection accepted on " + io); - if (io.getConfig() instanceof SocketSessionConfig) { - final SocketSessionConfig c = (SocketSessionConfig) io.getConfig(); - c.setKeepAlive(true); - } - ServerSession s = (ServerSession) super.createSession(io); - SocketAddress peer = io.getRemoteAddress(); - SshSession session = new SshSession(idGenerator.next(), peer); - s.setAttribute(SshSession.KEY, session); - io.getCloseFuture().addListener(new IoFutureListener() { - @Override - public void operationComplete(IoFuture future) { - log.info("connection closed on " + io); - } - }); - return s; - } - }; - } - - public int getPort() { - return myAddress.getPort(); - } - - public String formatUrl(String gituser, String servername, String repository) { - if (getPort() == DEFAULT_PORT) { - // standard port - return MessageFormat.format("{0}@{1}/{2}", gituser, servername, repository); - } else { - // non-standard port - return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", gituser, servername, getPort(), repository); - } - } - - /** - * Start this daemon on a background thread. - * - * @throws IOException - * the server socket could not be opened. - * @throws IllegalStateException - * the daemon is already running. - */ - public synchronized void start() throws IOException { - if (run.get()) { - throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); - } - - super.start(); - run.set(true); - - log.info(MessageFormat.format("SSH Daemon is listening on {0}:{1,number,0}", - myAddress.getAddress().getHostAddress(), myAddress.getPort())); - } - - /** @return true if this daemon is receiving connections. */ - public boolean isRunning() { - return run.get(); - } - - /** Stop this daemon. */ - public synchronized void stop() { - if (run.get()) { - log.info("SSH Daemon stopping..."); - run.set(false); - - try { - super.stop(); - } catch (InterruptedException e) { - log.error("SSH Daemon stop interrupted", e); - } - } - } - - /** - * Performs most of default configuration (setup random sources, setup ciphers, - * etc; also, support for forwarding and filesystem is explicitly disallowed). - * - * {@link #setKeyPairProvider(KeyPairProvider)} and - * {@link #setPublickeyAuthenticator(PublickeyAuthenticator)} are left for you. - * And applying {@link #setCommandFactory(CommandFactory)} is probably wise if you - * want something to actually happen when users do successfully authenticate. - */ - @SuppressWarnings("unchecked") - public void setup() { - if (!SecurityUtils.isBouncyCastleRegistered()) - throw new RuntimeException("BC crypto not available"); - - setKeyExchangeFactories(Arrays.>asList( - new DHG14.Factory(), - new DHG1.Factory()) - ); - - setRandomFactory(new SingletonRandomFactory(new BouncyCastleRandom.Factory())); - - setupCiphers(); - - setCompressionFactories(Arrays.>asList( - new CompressionNone.Factory()) - ); - - setMacFactories(Arrays.>asList( - new HMACMD5.Factory(), - new HMACSHA1.Factory(), - new HMACMD596.Factory(), - new HMACSHA196.Factory()) - ); - - setChannelFactories(Arrays.>asList( - new ChannelSession.Factory(), - new ChannelDirectTcpip.Factory()) - ); - - setSignatureFactories(Arrays.>asList( - new SignatureDSA.Factory(), - new SignatureRSA.Factory()) - ); - - setFileSystemFactory(new FileSystemFactory() { - @Override - public FileSystemView createFileSystemView(Session session) throws IOException { - return new FileSystemView() { - @Override - public SshFile getFile(SshFile baseDir, String file) { - return null; - } - - @Override - public SshFile getFile(String file) { - return null; - } - }; - } - }); - - setForwardingFilter(new ForwardingFilter() { - @Override - public boolean canForwardAgent(ServerSession session) { - return false; - } - - @Override - public boolean canForwardX11(ServerSession session) { - return false; - } - - @Override - public boolean canConnect(InetSocketAddress address, ServerSession session) { - return false; - } - - @Override - public boolean canListen(InetSocketAddress address, ServerSession session) { - return false; - } - }); - - setUserAuthFactories(Arrays.>asList( - new UserAuthPublicKey.Factory()) - ); - } - - protected void setupCiphers() { - List> avail = new LinkedList>(); - avail.add(new AES128CBC.Factory()); - avail.add(new TripleDESCBC.Factory()); - avail.add(new BlowfishCBC.Factory()); - avail.add(new AES192CBC.Factory()); - avail.add(new AES256CBC.Factory()); - - for (Iterator> i = avail.iterator(); i.hasNext();) { - final NamedFactory f = i.next(); - try { - final Cipher c = f.create(); - final byte[] key = new byte[c.getBlockSize()]; - final byte[] iv = new byte[c.getIVSize()]; - c.init(Cipher.Mode.Encrypt, key, iv); - } catch (InvalidKeyException e) { - i.remove(); - } catch (Exception e) { - i.remove(); - } - } - setCipherFactories(avail); - } -} +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.security.InvalidKeyException; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.inject.Inject; + +import org.apache.mina.core.future.IoFuture; +import org.apache.mina.core.future.IoFutureListener; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.transport.socket.SocketSessionConfig; +import org.apache.sshd.SshServer; +import org.apache.sshd.common.Channel; +import org.apache.sshd.common.Cipher; +import org.apache.sshd.common.Compression; +import org.apache.sshd.common.KeyExchange; +import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.Mac; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.Signature; +import org.apache.sshd.common.cipher.AES128CBC; +import org.apache.sshd.common.cipher.AES192CBC; +import org.apache.sshd.common.cipher.AES256CBC; +import org.apache.sshd.common.cipher.BlowfishCBC; +import org.apache.sshd.common.cipher.TripleDESCBC; +import org.apache.sshd.common.compression.CompressionNone; +import org.apache.sshd.common.mac.HMACMD5; +import org.apache.sshd.common.mac.HMACMD596; +import org.apache.sshd.common.mac.HMACSHA1; +import org.apache.sshd.common.mac.HMACSHA196; +import org.apache.sshd.common.random.BouncyCastleRandom; +import org.apache.sshd.common.random.SingletonRandomFactory; +import org.apache.sshd.common.signature.SignatureDSA; +import org.apache.sshd.common.signature.SignatureRSA; +import org.apache.sshd.common.util.SecurityUtils; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.FileSystemFactory; +import org.apache.sshd.server.FileSystemView; +import org.apache.sshd.server.ForwardingFilter; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.SshFile; +import org.apache.sshd.server.UserAuth; +import org.apache.sshd.server.auth.UserAuthPublicKey; +import org.apache.sshd.server.channel.ChannelDirectTcpip; +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.kex.DHG1; +import org.apache.sshd.server.kex.DHG14; +import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.session.SessionFactory; +import org.eclipse.jgit.internal.JGitText; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.manager.IGitblit; +import com.gitblit.utils.IdGenerator; +import com.gitblit.utils.StringUtils; + +/** + * Manager for the ssh transport. Roughly analogous to the + * {@link com.gitblit.git.GitDaemon} class. + * + * @author Eric Myhre + * + */ +public class SshDaemon extends SshServer { + + private final Logger log = LoggerFactory.getLogger(SshDaemon.class); + + /** + * 22: IANA assigned port number for ssh. Note that this is a distinct + * concept from gitblit's default conf for ssh port -- this "default" is + * what the git protocol itself defaults to if it sees and ssh url without a + * port. + */ + public static final int DEFAULT_PORT = 22; + + private static final String HOST_KEY_STORE = "sshKeyStore.pem"; + + private InetSocketAddress myAddress; + + private AtomicBoolean run; + + @SuppressWarnings("unused") + private IGitblit gitblit; + + /** + * Construct the Gitblit SSH daemon. + * + * @param gitblit + */ + @Inject + SshDaemon(IGitblit gitblit, IdGenerator idGenerator, + SshCommandFactory factory) { + this.gitblit = gitblit; + IStoredSettings settings = gitblit.getSettings(); + int port = settings.getInteger(Keys.git.sshPort, 0); + String bindInterface = settings.getString(Keys.git.sshBindInterface, + "localhost"); + + if (StringUtils.isEmpty(bindInterface)) { + myAddress = new InetSocketAddress(port); + } else { + myAddress = new InetSocketAddress(bindInterface, port); + } + + setPort(myAddress.getPort()); + setHost(myAddress.getHostName()); + setup(); + setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( + gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); + setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); + + run = new AtomicBoolean(false); + setCommandFactory(factory); + setSessionFactory(newSessionFactory(idGenerator)); + } + + SessionFactory newSessionFactory(final IdGenerator idGenerator) { + return new SessionFactory() { + @Override + protected ServerSession createSession(final IoSession io) + throws Exception { + log.info("connection accepted on " + io); + if (io.getConfig() instanceof SocketSessionConfig) { + final SocketSessionConfig c = (SocketSessionConfig) io + .getConfig(); + c.setKeepAlive(true); + } + ServerSession s = (ServerSession) super.createSession(io); + SocketAddress peer = io.getRemoteAddress(); + SshSession session = new SshSession(idGenerator.next(), peer); + s.setAttribute(SshSession.KEY, session); + io.getCloseFuture().addListener( + new IoFutureListener() { + @Override + public void operationComplete(IoFuture future) { + log.info("connection closed on " + io); + } + }); + return s; + } + }; + } + + public int getPort() { + return myAddress.getPort(); + } + + public String formatUrl(String gituser, String servername, String repository) { + if (getPort() == DEFAULT_PORT) { + // standard port + return MessageFormat.format("{0}@{1}/{2}", gituser, servername, + repository); + } else { + // non-standard port + return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", + gituser, servername, getPort(), repository); + } + } + + /** + * Start this daemon on a background thread. + * + * @throws IOException + * the server socket could not be opened. + * @throws IllegalStateException + * the daemon is already running. + */ + public synchronized void start() throws IOException { + if (run.get()) { + throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); + } + + super.start(); + run.set(true); + + log.info(MessageFormat.format( + "SSH Daemon is listening on {0}:{1,number,0}", myAddress + .getAddress().getHostAddress(), myAddress.getPort())); + } + + /** @return true if this daemon is receiving connections. */ + public boolean isRunning() { + return run.get(); + } + + /** Stop this daemon. */ + public synchronized void stop() { + if (run.get()) { + log.info("SSH Daemon stopping..."); + run.set(false); + + try { + super.stop(); + } catch (InterruptedException e) { + log.error("SSH Daemon stop interrupted", e); + } + } + } + + /** + * Performs most of default configuration (setup random sources, setup + * ciphers, etc; also, support for forwarding and filesystem is explicitly + * disallowed). + * + * {@link #setKeyPairProvider(KeyPairProvider)} and + * {@link #setPublickeyAuthenticator(PublickeyAuthenticator)} are left for + * you. And applying {@link #setCommandFactory(CommandFactory)} is probably + * wise if you want something to actually happen when users do successfully + * authenticate. + */ + @SuppressWarnings("unchecked") + public void setup() { + if (!SecurityUtils.isBouncyCastleRegistered()) + throw new RuntimeException("BC crypto not available"); + + setKeyExchangeFactories(Arrays.> asList( + new DHG14.Factory(), new DHG1.Factory())); + + setRandomFactory(new SingletonRandomFactory( + new BouncyCastleRandom.Factory())); + + setupCiphers(); + + setCompressionFactories(Arrays + .> asList(new CompressionNone.Factory())); + + setMacFactories(Arrays.> asList( + new HMACMD5.Factory(), new HMACSHA1.Factory(), + new HMACMD596.Factory(), new HMACSHA196.Factory())); + + setChannelFactories(Arrays.> asList( + new ChannelSession.Factory(), new ChannelDirectTcpip.Factory())); + + setSignatureFactories(Arrays.> asList( + new SignatureDSA.Factory(), new SignatureRSA.Factory())); + + setFileSystemFactory(new FileSystemFactory() { + @Override + public FileSystemView createFileSystemView(Session session) + throws IOException { + return new FileSystemView() { + @Override + public SshFile getFile(SshFile baseDir, String file) { + return null; + } + + @Override + public SshFile getFile(String file) { + return null; + } + }; + } + }); + + setForwardingFilter(new ForwardingFilter() { + @Override + public boolean canForwardAgent(ServerSession session) { + return false; + } + + @Override + public boolean canForwardX11(ServerSession session) { + return false; + } + + @Override + public boolean canConnect(InetSocketAddress address, + ServerSession session) { + return false; + } + + @Override + public boolean canListen(InetSocketAddress address, + ServerSession session) { + return false; + } + }); + + setUserAuthFactories(Arrays + .> asList(new UserAuthPublicKey.Factory())); + } + + protected void setupCiphers() { + List> avail = new LinkedList>(); + avail.add(new AES128CBC.Factory()); + avail.add(new TripleDESCBC.Factory()); + avail.add(new BlowfishCBC.Factory()); + avail.add(new AES192CBC.Factory()); + avail.add(new AES256CBC.Factory()); + + for (Iterator> i = avail.iterator(); i.hasNext();) { + final NamedFactory f = i.next(); + try { + final Cipher c = f.create(); + final byte[] key = new byte[c.getBlockSize()]; + final byte[] iv = new byte[c.getIVSize()]; + c.init(Cipher.Mode.Encrypt, key, iv); + } catch (InvalidKeyException e) { + i.remove(); + } catch (Exception e) { + i.remove(); + } + } + setCipherFactories(avail); + } +} -- cgit v1.2.3 From 924c9b28edfda6582a5480a7c1a2dd5b3ce89d92 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 22 Feb 2014 16:19:03 -0500 Subject: Eliminate use of Dagger and do not create inner classes This change outlines somethings I'd like to see in your latest code. 1. Elimination of Dagger 2. Less noise from inner class instantiation 3. Formalizing filesystem location for keys instead of Unix-y /tmp/ 4. Password authentication as alternative to Key authentication Change-Id: I7a76e1811ad7cd545444fd8b59bbe8c4f45ccfec --- .../java/com/gitblit/manager/ServicesManager.java | 57 +---- .../gitblit/transport/ssh/CommandDispatcher.java | 44 ---- .../transport/ssh/DisabledFilesystemFactory.java | 26 ++ .../gitblit/transport/ssh/NonForwardingFilter.java | 28 +++ .../gitblit/transport/ssh/SshCommandFactory.java | 27 ++- .../java/com/gitblit/transport/ssh/SshDaemon.java | 265 ++++----------------- .../transport/ssh/SshPasswordAuthenticator.java | 50 ++++ .../gitblit/transport/ssh/SshSessionFactory.java | 69 ++++++ .../transport/ssh/commands/DispatchCommand.java | 52 ++-- src/main/java/com/gitblit/utils/WorkQueue.java | 38 +-- 10 files changed, 293 insertions(+), 363 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java create mode 100644 src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java create mode 100644 src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java (limited to 'src') diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index 219e4ea5..df8918ed 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -24,13 +24,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import javax.inject.Named; -import javax.inject.Singleton; import javax.servlet.http.HttpServletRequest; -import org.apache.sshd.server.Command; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,26 +38,15 @@ import com.gitblit.fanout.FanoutNioService; import com.gitblit.fanout.FanoutService; import com.gitblit.fanout.FanoutSocketService; import com.gitblit.git.GitDaemon; -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.models.FederationModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.service.FederationPullService; -import com.gitblit.transport.ssh.SshCommandFactory; import com.gitblit.transport.ssh.SshDaemon; -import com.gitblit.transport.ssh.SshSession; -import com.gitblit.transport.ssh.commands.CreateRepository; -import com.gitblit.transport.ssh.commands.VersionCommand; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; import com.gitblit.utils.TimeUtils; -import dagger.Module; -import dagger.ObjectGraph; -import dagger.Provides; - /** * Services manager manages long-running services/processes that either have no * direct relation to other managers OR require really high-level manager @@ -111,6 +95,9 @@ public class ServicesManager implements IManager { if (gitDaemon != null) { gitDaemon.stop(); } + if (sshDaemon != null) { + sshDaemon.stop(); + } return this; } @@ -164,7 +151,7 @@ public class ServicesManager implements IManager { String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); if (port > 0) { try { - sshDaemon = ObjectGraph.create(new SshModule()).get(SshDaemon.class); + sshDaemon = new SshDaemon(gitblit, new IdGenerator()); sshDaemon.start(); } catch (IOException e) { sshDaemon = null; @@ -262,40 +249,4 @@ public class ServicesManager implements IManager { } } - - @Module(library = true, - injects = { - IGitblit.class, - SshCommandFactory.class, - SshDaemon.class, - }) - public class SshModule { - @Provides @Named("create-repository") Command provideCreateRepository() { - return new CreateRepository(); - } - - @Provides @Named("version") Command provideVersion() { - return new VersionCommand(); - } - - @Provides @Singleton IdGenerator provideIdGenerator() { - return new IdGenerator(); - } - - @Provides @Singleton RepositoryResolver provideRepositoryResolver() { - return new RepositoryResolver(provideGitblit()); - } - - @Provides @Singleton UploadPackFactory provideUploadPackFactory() { - return new GitblitUploadPackFactory(provideGitblit()); - } - - @Provides @Singleton ReceivePackFactory provideReceivePackFactory() { - return new GitblitReceivePackFactory(provideGitblit()); - } - - @Provides @Singleton IGitblit provideGitblit() { - return ServicesManager.this.gitblit; - } - } } diff --git a/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java b/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java deleted file mode 100644 index 18c1c331..00000000 --- a/src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.gitblit.transport.ssh; - -import java.util.Map; -import java.util.Set; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; - -import org.apache.sshd.server.Command; - -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -public class CommandDispatcher extends DispatchCommand { - - Provider repo; - Provider version; - - @Inject - public CommandDispatcher(final @Named("create-repository") Provider repo, - final @Named("version") Provider version) { - this.repo = repo; - this.version = version; - } - - public DispatchCommand get() { - DispatchCommand root = new DispatchCommand(); - Map> origin = Maps.newHashMapWithExpectedSize(2); - origin.put("gitblit", new Provider() { - @Override - public Command get() { - Set> gitblit = Sets.newHashSetWithExpectedSize(2); - gitblit.add(repo); - gitblit.add(version); - Command cmd = new DispatchCommand(gitblit); - return cmd; - } - }); - root.setMap(origin); - return root; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java new file mode 100644 index 00000000..32a896b8 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java @@ -0,0 +1,26 @@ +package com.gitblit.transport.ssh; + +import java.io.IOException; + +import org.apache.sshd.common.Session; +import org.apache.sshd.server.FileSystemFactory; +import org.apache.sshd.server.FileSystemView; +import org.apache.sshd.server.SshFile; + +public class DisabledFilesystemFactory implements FileSystemFactory { + + @Override + public FileSystemView createFileSystemView(Session session) throws IOException { + return new FileSystemView() { + @Override + public SshFile getFile(SshFile baseDir, String file) { + return null; + } + + @Override + public SshFile getFile(String file) { + return null; + } + }; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java new file mode 100644 index 00000000..82f84267 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java @@ -0,0 +1,28 @@ +package com.gitblit.transport.ssh; + +import java.net.InetSocketAddress; + +import org.apache.sshd.server.ForwardingFilter; +import org.apache.sshd.server.session.ServerSession; + +public class NonForwardingFilter implements ForwardingFilter { + @Override + public boolean canForwardAgent(ServerSession session) { + return false; + } + + @Override + public boolean canForwardX11(ServerSession session) { + return false; + } + + @Override + public boolean canConnect(InetSocketAddress address, ServerSession session) { + return false; + } + + @Override + public boolean canListen(InetSocketAddress address, ServerSession session) { + return false; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index 85c503d4..056938e9 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -25,8 +25,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import javax.inject.Inject; - import org.apache.sshd.server.Command; import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.Environment; @@ -66,14 +64,13 @@ public class SshCommandFactory implements CommandFactory { private ReceivePackFactory receivePackFactory; private final ScheduledExecutorService startExecutor; - private CommandDispatcher dispatcher; + private DispatchCommand dispatcher; - @Inject public SshCommandFactory(RepositoryResolver repositoryResolver, UploadPackFactory uploadPackFactory, ReceivePackFactory receivePackFactory, WorkQueue workQueue, - CommandDispatcher d) { + DispatchCommand d) { this.repositoryResolver = repositoryResolver; this.uploadPackFactory = uploadPackFactory; this.receivePackFactory = receivePackFactory; @@ -116,26 +113,32 @@ public class SshCommandFactory implements CommandFactory { // TODO Auto-generated method stub } - public void setInputStream(final InputStream in) { + @Override + public void setInputStream(final InputStream in) { this.in = in; } - public void setOutputStream(final OutputStream out) { + @Override + public void setOutputStream(final OutputStream out) { this.out = out; } - public void setErrorStream(final OutputStream err) { + @Override + public void setErrorStream(final OutputStream err) { this.err = err; } - public void setExitCallback(final ExitCallback callback) { + @Override + public void setExitCallback(final ExitCallback callback) { this.exit = callback; } - public void start(final Environment env) throws IOException { + @Override + public void start(final Environment env) throws IOException { this.env = env; task.set(startExecutor.submit(new Runnable() { - public void run() { + @Override + public void run() { try { onStart(); } catch (Exception e) { @@ -155,7 +158,7 @@ public class SshCommandFactory implements CommandFactory { synchronized (this) { //final Context old = sshScope.set(ctx); try { - cmd = dispatcher.get(); + cmd = dispatcher; cmd.setArguments(argv); cmd.setInputStream(in); cmd.setOutputStream(out); diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index b3471a29..42ee67ab 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -18,79 +18,36 @@ package com.gitblit.transport.ssh; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.security.InvalidKeyException; import java.text.MessageFormat; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import javax.inject.Inject; - -import org.apache.mina.core.future.IoFuture; -import org.apache.mina.core.future.IoFutureListener; -import org.apache.mina.core.session.IoSession; -import org.apache.mina.transport.socket.SocketSessionConfig; import org.apache.sshd.SshServer; -import org.apache.sshd.common.Channel; -import org.apache.sshd.common.Cipher; -import org.apache.sshd.common.Compression; -import org.apache.sshd.common.KeyExchange; -import org.apache.sshd.common.KeyPairProvider; -import org.apache.sshd.common.Mac; -import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.Session; -import org.apache.sshd.common.Signature; -import org.apache.sshd.common.cipher.AES128CBC; -import org.apache.sshd.common.cipher.AES192CBC; -import org.apache.sshd.common.cipher.AES256CBC; -import org.apache.sshd.common.cipher.BlowfishCBC; -import org.apache.sshd.common.cipher.TripleDESCBC; -import org.apache.sshd.common.compression.CompressionNone; -import org.apache.sshd.common.mac.HMACMD5; -import org.apache.sshd.common.mac.HMACMD596; -import org.apache.sshd.common.mac.HMACSHA1; -import org.apache.sshd.common.mac.HMACSHA196; -import org.apache.sshd.common.random.BouncyCastleRandom; -import org.apache.sshd.common.random.SingletonRandomFactory; -import org.apache.sshd.common.signature.SignatureDSA; -import org.apache.sshd.common.signature.SignatureRSA; -import org.apache.sshd.common.util.SecurityUtils; -import org.apache.sshd.server.CommandFactory; -import org.apache.sshd.server.FileSystemFactory; -import org.apache.sshd.server.FileSystemView; -import org.apache.sshd.server.ForwardingFilter; -import org.apache.sshd.server.PublickeyAuthenticator; -import org.apache.sshd.server.SshFile; -import org.apache.sshd.server.UserAuth; -import org.apache.sshd.server.auth.UserAuthPublicKey; -import org.apache.sshd.server.channel.ChannelDirectTcpip; -import org.apache.sshd.server.channel.ChannelSession; -import org.apache.sshd.server.kex.DHG1; -import org.apache.sshd.server.kex.DHG14; import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; -import org.apache.sshd.server.session.ServerSession; -import org.apache.sshd.server.session.SessionFactory; import org.eclipse.jgit.internal.JGitText; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.IStoredSettings; import com.gitblit.Keys; +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; +import com.gitblit.transport.ssh.commands.CreateRepository; +import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.VersionCommand; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; +import com.gitblit.utils.WorkQueue; /** * Manager for the ssh transport. Roughly analogous to the * {@link com.gitblit.git.GitDaemon} class. - * + * * @author Eric Myhre - * + * */ -public class SshDaemon extends SshServer { +public class SshDaemon { private final Logger log = LoggerFactory.getLogger(SshDaemon.class); @@ -104,91 +61,78 @@ public class SshDaemon extends SshServer { private static final String HOST_KEY_STORE = "sshKeyStore.pem"; - private InetSocketAddress myAddress; - - private AtomicBoolean run; + private final AtomicBoolean run; @SuppressWarnings("unused") - private IGitblit gitblit; + private final IGitblit gitblit; + + private final IdGenerator idGenerator; + + private final SshServer sshd; /** * Construct the Gitblit SSH daemon. - * + * * @param gitblit */ - @Inject - SshDaemon(IGitblit gitblit, IdGenerator idGenerator, - SshCommandFactory factory) { + public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) { this.gitblit = gitblit; + this.idGenerator = idGenerator; + IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); + InetSocketAddress addr; if (StringUtils.isEmpty(bindInterface)) { - myAddress = new InetSocketAddress(port); + addr = new InetSocketAddress(port); } else { - myAddress = new InetSocketAddress(bindInterface, port); + addr = new InetSocketAddress(bindInterface, port); } - setPort(myAddress.getPort()); - setHost(myAddress.getHostName()); - setup(); - setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(addr.getPort()); + sshd.setHost(addr.getHostName()); + sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); - setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); + sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); + sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit)); + sshd.setSessionFactory(new SshSessionFactory(idGenerator)); + sshd.setFileSystemFactory(new DisabledFilesystemFactory()); + sshd.setForwardingFilter(new NonForwardingFilter()); - run = new AtomicBoolean(false); - setCommandFactory(factory); - setSessionFactory(newSessionFactory(idGenerator)); - } + DispatchCommand dispatcher = new DispatchCommand(); + dispatcher.registerCommand(CreateRepository.class); + dispatcher.registerCommand(VersionCommand.class); - SessionFactory newSessionFactory(final IdGenerator idGenerator) { - return new SessionFactory() { - @Override - protected ServerSession createSession(final IoSession io) - throws Exception { - log.info("connection accepted on " + io); - if (io.getConfig() instanceof SocketSessionConfig) { - final SocketSessionConfig c = (SocketSessionConfig) io - .getConfig(); - c.setKeepAlive(true); - } - ServerSession s = (ServerSession) super.createSession(io); - SocketAddress peer = io.getRemoteAddress(); - SshSession session = new SshSession(idGenerator.next(), peer); - s.setAttribute(SshSession.KEY, session); - io.getCloseFuture().addListener( - new IoFutureListener() { - @Override - public void operationComplete(IoFuture future) { - log.info("connection closed on " + io); - } - }); - return s; - } - }; - } + SshCommandFactory commandFactory = new SshCommandFactory( + new RepositoryResolver(gitblit), + new GitblitUploadPackFactory(gitblit), + new GitblitReceivePackFactory(gitblit), + new WorkQueue(idGenerator), + dispatcher); - public int getPort() { - return myAddress.getPort(); + sshd.setCommandFactory(commandFactory); + + run = new AtomicBoolean(false); } public String formatUrl(String gituser, String servername, String repository) { - if (getPort() == DEFAULT_PORT) { + if (sshd.getPort() == DEFAULT_PORT) { // standard port return MessageFormat.format("{0}@{1}/{2}", gituser, servername, repository); } else { // non-standard port return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", - gituser, servername, getPort(), repository); + gituser, servername, sshd.getPort(), repository); } } /** * Start this daemon on a background thread. - * + * * @throws IOException * the server socket could not be opened. * @throws IllegalStateException @@ -199,12 +143,12 @@ public class SshDaemon extends SshServer { throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); } - super.start(); + sshd.start(); run.set(true); log.info(MessageFormat.format( - "SSH Daemon is listening on {0}:{1,number,0}", myAddress - .getAddress().getHostAddress(), myAddress.getPort())); + "SSH Daemon is listening on {0}:{1,number,0}", + sshd.getHost(), sshd.getPort())); } /** @return true if this daemon is receiving connections. */ @@ -219,117 +163,10 @@ public class SshDaemon extends SshServer { run.set(false); try { - super.stop(); + sshd.stop(); } catch (InterruptedException e) { log.error("SSH Daemon stop interrupted", e); } } } - - /** - * Performs most of default configuration (setup random sources, setup - * ciphers, etc; also, support for forwarding and filesystem is explicitly - * disallowed). - * - * {@link #setKeyPairProvider(KeyPairProvider)} and - * {@link #setPublickeyAuthenticator(PublickeyAuthenticator)} are left for - * you. And applying {@link #setCommandFactory(CommandFactory)} is probably - * wise if you want something to actually happen when users do successfully - * authenticate. - */ - @SuppressWarnings("unchecked") - public void setup() { - if (!SecurityUtils.isBouncyCastleRegistered()) - throw new RuntimeException("BC crypto not available"); - - setKeyExchangeFactories(Arrays.> asList( - new DHG14.Factory(), new DHG1.Factory())); - - setRandomFactory(new SingletonRandomFactory( - new BouncyCastleRandom.Factory())); - - setupCiphers(); - - setCompressionFactories(Arrays - .> asList(new CompressionNone.Factory())); - - setMacFactories(Arrays.> asList( - new HMACMD5.Factory(), new HMACSHA1.Factory(), - new HMACMD596.Factory(), new HMACSHA196.Factory())); - - setChannelFactories(Arrays.> asList( - new ChannelSession.Factory(), new ChannelDirectTcpip.Factory())); - - setSignatureFactories(Arrays.> asList( - new SignatureDSA.Factory(), new SignatureRSA.Factory())); - - setFileSystemFactory(new FileSystemFactory() { - @Override - public FileSystemView createFileSystemView(Session session) - throws IOException { - return new FileSystemView() { - @Override - public SshFile getFile(SshFile baseDir, String file) { - return null; - } - - @Override - public SshFile getFile(String file) { - return null; - } - }; - } - }); - - setForwardingFilter(new ForwardingFilter() { - @Override - public boolean canForwardAgent(ServerSession session) { - return false; - } - - @Override - public boolean canForwardX11(ServerSession session) { - return false; - } - - @Override - public boolean canConnect(InetSocketAddress address, - ServerSession session) { - return false; - } - - @Override - public boolean canListen(InetSocketAddress address, - ServerSession session) { - return false; - } - }); - - setUserAuthFactories(Arrays - .> asList(new UserAuthPublicKey.Factory())); - } - - protected void setupCiphers() { - List> avail = new LinkedList>(); - avail.add(new AES128CBC.Factory()); - avail.add(new TripleDESCBC.Factory()); - avail.add(new BlowfishCBC.Factory()); - avail.add(new AES192CBC.Factory()); - avail.add(new AES256CBC.Factory()); - - for (Iterator> i = avail.iterator(); i.hasNext();) { - final NamedFactory f = i.next(); - try { - final Cipher c = f.create(); - final byte[] key = new byte[c.getBlockSize()]; - final byte[] iv = new byte[c.getIVSize()]; - c.init(Cipher.Mode.Encrypt, key, iv); - } catch (InvalidKeyException e) { - i.remove(); - } catch (Exception e) { - i.remove(); - } - } - setCipherFactories(avail); - } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java new file mode 100644 index 00000000..e39b5f72 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.util.Locale; + +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; + +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; + +/** + * + * @author James Moger + * + */ +public class SshPasswordAuthenticator implements PasswordAuthenticator { + + protected final IGitblit gitblit; + + public SshPasswordAuthenticator(IGitblit gitblit) { + this.gitblit = gitblit; + } + + @Override + public boolean authenticate(String username, String password, ServerSession session) { + username = username.toLowerCase(Locale.US); + UserModel user = gitblit.authenticate(username, password.toCharArray()); + if (user != null) { + SshSession sd = session.getAttribute(SshSession.KEY); + sd.authenticationSuccess(username); + return true; + } + return false; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java new file mode 100644 index 00000000..ef513404 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.net.SocketAddress; + +import org.apache.mina.core.future.IoFuture; +import org.apache.mina.core.future.IoFutureListener; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.transport.socket.SocketSessionConfig; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.session.SessionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.utils.IdGenerator; + + +/** + * + * @author James Moger + * + */ +public class SshSessionFactory extends SessionFactory { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final IdGenerator idGenerator; + + public SshSessionFactory(IdGenerator idGenerator) { + this.idGenerator = idGenerator; + } + + @Override + protected ServerSession createSession(final IoSession io) throws Exception { + log.info("connection accepted on " + io); + + if (io.getConfig() instanceof SocketSessionConfig) { + final SocketSessionConfig c = (SocketSessionConfig) io.getConfig(); + c.setKeepAlive(true); + } + + final ServerSession s = (ServerSession) super.createSession(io); + SocketAddress peer = io.getRemoteAddress(); + SshSession session = new SshSession(idGenerator.next(), peer); + s.setAttribute(SshSession.KEY, session); + + io.getCloseFuture().addListener(new IoFutureListener() { + @Override + public void operationComplete(IoFuture future) { + log.info("connection closed on " + io); + } + }); + return s; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 672f0245..0d614a0f 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -16,13 +16,13 @@ package com.gitblit.transport.ssh.commands; import java.io.IOException; import java.io.StringWriter; +import java.text.MessageFormat; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import javax.inject.Provider; - import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.kohsuke.args4j.Argument; @@ -42,28 +42,26 @@ public class DispatchCommand extends BaseCommand { @Argument(index = 1, multiValued = true, metaVar = "ARG") private List args = new ArrayList(); - private Set> commands; - private Map> map; - - public DispatchCommand() {} - - public DispatchCommand(Map> map) { - this.map = map; - } + private Set> commands; + private Map> map; - public void setMap(Map> m) { - map = m; + public DispatchCommand() { + commands = new HashSet>(); } - public DispatchCommand(Set> commands) { - this.commands = commands; + public void registerCommand(Class cmd) { + if (!cmd.isAnnotationPresent(CommandMetaData.class)) { + throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", + cmd.getName(), CommandMetaData.class.getName())); + } + commands.add(cmd); } - private Map> getMap() { + private Map> getMap() { if (map == null) { map = Maps.newHashMapWithExpectedSize(commands.size()); - for (Provider cmd : commands) { - CommandMetaData meta = cmd.get().getClass().getAnnotation(CommandMetaData.class); + for (Class cmd : commands) { + CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); map.put(meta.name(), cmd); } } @@ -80,15 +78,20 @@ public class DispatchCommand extends BaseCommand { throw new UnloggedFailure(1, msg.toString()); } - final Provider p = getMap().get(commandName); - if (p == null) { + final Class c = getMap().get(commandName); + if (c == null) { String msg = (getName().isEmpty() ? "Gitblit" : getName()) + ": " + commandName + ": not found"; throw new UnloggedFailure(1, msg); } - final Command cmd = p.get(); + Command cmd = null; + try { + cmd = c.newInstance(); + } catch (Exception e) { + throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName)); + } if (cmd instanceof BaseCommand) { BaseCommand bc = (BaseCommand) cmd; if (getName().isEmpty()) { @@ -116,7 +119,8 @@ public class DispatchCommand extends BaseCommand { } } - protected String usage() { + @Override +protected String usage() { final StringBuilder usage = new StringBuilder(); usage.append("Available commands"); if (!getName().isEmpty()) { @@ -127,15 +131,15 @@ public class DispatchCommand extends BaseCommand { usage.append("\n"); int maxLength = -1; - Map> m = getMap(); + Map> m = getMap(); for (String name : m.keySet()) { maxLength = Math.max(maxLength, name.length()); } String format = "%-" + maxLength + "s %s"; for (String name : Sets.newTreeSet(m.keySet())) { - final Provider p = m.get(name); + final Class c = m.get(name); usage.append(" "); - CommandMetaData meta = p.get().getClass().getAnnotation(CommandMetaData.class); + CommandMetaData meta = c.getAnnotation(CommandMetaData.class); if (meta != null) { usage.append(String.format(format, name, Strings.nullToEmpty(meta.description()))); diff --git a/src/main/java/com/gitblit/utils/WorkQueue.java b/src/main/java/com/gitblit/utils/WorkQueue.java index 778e754c..ba49a4c5 100644 --- a/src/main/java/com/gitblit/utils/WorkQueue.java +++ b/src/main/java/com/gitblit/utils/WorkQueue.java @@ -14,11 +14,6 @@ package com.gitblit.utils; -import com.google.common.collect.Lists; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; import java.util.Collection; @@ -38,7 +33,10 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import javax.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.Lists; /** Delayed execution of tasks using a background thread pool. */ public class WorkQueue { @@ -55,7 +53,6 @@ public class WorkQueue { private final IdGenerator idGenerator; private final CopyOnWriteArrayList queues; - @Inject public WorkQueue(final IdGenerator idGenerator) { this.idGenerator = idGenerator; this.queues = new CopyOnWriteArrayList(); @@ -268,7 +265,8 @@ public class WorkQueue { return startTime; } - public boolean cancel(boolean mayInterruptIfRunning) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { if (task.cancel(mayInterruptIfRunning)) { // Tiny abuse of running: if the task needs to know it was // canceled (to clean up resources) and it hasn't started @@ -289,36 +287,44 @@ public class WorkQueue { } } - public int compareTo(Delayed o) { + @Override + public int compareTo(Delayed o) { return task.compareTo(o); } - public V get() throws InterruptedException, ExecutionException { + @Override + public V get() throws InterruptedException, ExecutionException { return task.get(); } - public V get(long timeout, TimeUnit unit) throws InterruptedException, + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return task.get(timeout, unit); } - public long getDelay(TimeUnit unit) { + @Override + public long getDelay(TimeUnit unit) { return task.getDelay(unit); } - public boolean isCancelled() { + @Override + public boolean isCancelled() { return task.isCancelled(); } - public boolean isDone() { + @Override + public boolean isDone() { return task.isDone(); } - public boolean isPeriodic() { + @Override + public boolean isPeriodic() { return task.isPeriodic(); } - public void run() { + @Override + public void run() { if (running.compareAndSet(false, true)) { try { task.run(); -- cgit v1.2.3 From a3de33e71a22268105714e01d09c1c2e28bfe2c3 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Wed, 26 Feb 2014 08:19:44 +0100 Subject: Fix command dispatching DispatchCommand is supposed to be nested: ssh server gitblit version --verbose --format json means that first the command that is seen by dispatching process is "gitblit". Dispatch command look in its commands map for this command and dispatch the rest of the command and options and arguments to this command, version in this example. Change-Id: I8ef8e0e369922c793ca7ad36c1a8f76b0206baa7 --- .../java/com/gitblit/transport/ssh/SshDaemon.java | 7 +++- .../transport/ssh/commands/DispatchCommand.java | 44 +++++++++++++++------- 2 files changed, 35 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 42ee67ab..dd4a2d8e 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -102,9 +102,12 @@ public class SshDaemon { sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setForwardingFilter(new NonForwardingFilter()); + DispatchCommand gitblitCmd = new DispatchCommand(); + gitblitCmd.registerCommand(CreateRepository.class); + gitblitCmd.registerCommand(VersionCommand.class); + DispatchCommand dispatcher = new DispatchCommand(); - dispatcher.registerCommand(CreateRepository.class); - dispatcher.registerCommand(VersionCommand.class); + dispatcher.registerDispatcher("gitblit", gitblitCmd); SshCommandFactory commandFactory = new SshCommandFactory( new RepositoryResolver(gitblit), diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 0d614a0f..b6944eaf 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -44,11 +44,19 @@ public class DispatchCommand extends BaseCommand { private Set> commands; private Map> map; + private Map root; public DispatchCommand() { commands = new HashSet>(); } + public void registerDispatcher(String name, Command cmd) { + if (root == null) { + root = Maps.newHashMap(); + } + root.put(name, cmd); + } + public void registerCommand(Class cmd) { if (!cmd.isAnnotationPresent(CommandMetaData.class)) { throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", @@ -78,20 +86,7 @@ public class DispatchCommand extends BaseCommand { throw new UnloggedFailure(1, msg.toString()); } - final Class c = getMap().get(commandName); - if (c == null) { - String msg = - (getName().isEmpty() ? "Gitblit" : getName()) + ": " - + commandName + ": not found"; - throw new UnloggedFailure(1, msg); - } - - Command cmd = null; - try { - cmd = c.newInstance(); - } catch (Exception e) { - throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName)); - } + Command cmd = getCommand(); if (cmd instanceof BaseCommand) { BaseCommand bc = (BaseCommand) cmd; if (getName().isEmpty()) { @@ -119,6 +114,27 @@ public class DispatchCommand extends BaseCommand { } } + private Command getCommand() throws UnloggedFailure { + if (root != null && root.containsKey(commandName)) { + return root.get(commandName); + } + final Class c = getMap().get(commandName); + if (c == null) { + String msg = + (getName().isEmpty() ? "Gitblit" : getName()) + ": " + + commandName + ": not found"; + throw new UnloggedFailure(1, msg); + } + + Command cmd = null; + try { + cmd = c.newInstance(); + } catch (Exception e) { + throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName)); + } + return cmd; + } + @Override protected String usage() { final StringBuilder usage = new StringBuilder(); -- cgit v1.2.3 From e3b636e7fa2a823cfe90ea75e88034a60f7e59e6 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Sat, 22 Feb 2014 21:17:03 +0100 Subject: SSHD: Add support for git pack commands Add git-upload-pack and git-receive-pack commands. Conflicts: src/main/java/com/gitblit/manager/ServicesManager.java src/main/java/com/gitblit/transport/ssh/CommandDispatcher.java src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java Change-Id: I8c057b41f1dfad6d004e6aa91f96c8c673be9be2 --- src/main/java/com/gitblit/Constants.java | 2 +- .../com/gitblit/git/GitblitReceivePackFactory.java | 8 ++ .../java/com/gitblit/git/RepositoryResolver.java | 10 ++ .../com/gitblit/manager/AuthenticationManager.java | 29 ++++ .../java/com/gitblit/manager/GitblitManager.java | 7 + .../gitblit/manager/IAuthenticationManager.java | 3 + .../java/com/gitblit/manager/ServicesManager.java | 1 - .../gitblit/transport/ssh/AbstractGitCommand.java | 108 +++++++++++++++ .../gitblit/transport/ssh/SshCommandFactory.java | 147 ++------------------- .../java/com/gitblit/transport/ssh/SshContext.java | 35 +++++ .../java/com/gitblit/transport/ssh/SshDaemon.java | 24 ++-- .../java/com/gitblit/transport/ssh/SshSession.java | 9 ++ .../transport/ssh/commands/BaseCommand.java | 38 +++--- .../transport/ssh/commands/DispatchCommand.java | 48 ++++++- .../gitblit/transport/ssh/commands/Receive.java | 34 +++++ .../com/gitblit/transport/ssh/commands/Upload.java | 39 ++++++ .../transport/ssh/commands/VersionCommand.java | 2 +- 17 files changed, 379 insertions(+), 165 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshContext.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/Receive.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/Upload.java (limited to 'src') diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java index 2a98b53f..889e5a30 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java @@ -501,7 +501,7 @@ public class Constants { } public static enum AuthenticationType { - CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER; + SSH, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER; public boolean isStandard() { return ordinal() <= COOKIE.ordinal(); diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java index 7976fe56..9911258c 100644 --- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java @@ -31,6 +31,7 @@ import com.gitblit.Keys; import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.StringUtils; @@ -88,6 +89,13 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { // set timeout from Git daemon timeout = client.getDaemon().getTimeout(); + } else if (req instanceof SshSession) { + // SSH request is always authenticated + SshSession s = (SshSession) req; + repositoryName = s.getRepositoryName(); + origin = s.getRemoteAddress().toString(); + String username = s.getRemoteUser(); + user = gitblit.getUserModel(username); } boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false); diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java index 208c1ae1..c859f6f6 100644 --- a/src/main/java/com/gitblit/git/RepositoryResolver.java +++ b/src/main/java/com/gitblit/git/RepositoryResolver.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.SshSession; /** * Resolves repositories and grants export access. @@ -67,6 +68,9 @@ public class RepositoryResolver extends FileResolver { // git request GitDaemonClient client = (GitDaemonClient) req; client.setRepositoryName(name); + } else if (req instanceof SshSession) { + SshSession s = (SshSession)req; + s.setRepositoryName(name); } return repo; } @@ -98,6 +102,12 @@ public class RepositoryResolver extends FileResolver { if (user == null) { user = UserModel.ANONYMOUS; } + } else if (req instanceof SshSession) { + SshSession s = (SshSession) req; + user = gitblit.authenticate(s); + if (user == null) { + throw new IOException(String.format("User %s not found", s.getRemoteUser())); + } } if (user.canClone(model)) { diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java index 4f3e652c..47425ce7 100644 --- a/src/main/java/com/gitblit/manager/AuthenticationManager.java +++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java @@ -47,6 +47,7 @@ import com.gitblit.auth.SalesforceAuthProvider; import com.gitblit.auth.WindowsAuthProvider; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.Base64; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.StringUtils; @@ -289,6 +290,34 @@ public class AuthenticationManager implements IAuthenticationManager { return null; } + /** + * Authenticate a user based on SSH session. + * + * @param SshSession + * @return a user object or null + */ + @Override + public UserModel authenticate(SshSession sshSession) { + String username = sshSession.getRemoteUser(); + if (username != null) { + if (!StringUtils.isEmpty(username)) { + UserModel user = userManager.getUserModel(username); + if (user != null) { + // existing user + logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}", + user.username, sshSession.getRemoteAddress())); + return validateAuthentication(user, AuthenticationType.SSH); + } + logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted ssh authentication from {1}", + username, sshSession.getRemoteAddress())); + } + } else { + logger.warn("Empty user in SSH session"); + } + return null; + } + + /** * This method allows the authentication manager to reject authentication * attempts. It is called after the username/secret have been verified to diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index b6c2b474..a5a26379 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -68,6 +68,7 @@ import com.gitblit.models.SettingModel; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.tickets.ITicketService; +import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JsonUtils; @@ -651,6 +652,12 @@ public class GitblitManager implements IGitblit { } return user; } + + @Override + public UserModel authenticate(SshSession sshSession) { + return authenticationManager.authenticate(sshSession); + } + @Override public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) { UserModel user = authenticationManager.authenticate(httpRequest, requiresCertificate); diff --git a/src/main/java/com/gitblit/manager/IAuthenticationManager.java b/src/main/java/com/gitblit/manager/IAuthenticationManager.java index 3007a303..5d98d127 100644 --- a/src/main/java/com/gitblit/manager/IAuthenticationManager.java +++ b/src/main/java/com/gitblit/manager/IAuthenticationManager.java @@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletResponse; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.SshSession; public interface IAuthenticationManager extends IManager { @@ -33,6 +34,8 @@ public interface IAuthenticationManager extends IManager { */ UserModel authenticate(HttpServletRequest httpRequest); + public UserModel authenticate(SshSession sshSession); + /** * Authenticate a user based on HTTP request parameters. * diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index df8918ed..11083be3 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -247,6 +247,5 @@ public class ServicesManager implements IManager { "Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}", registration.name, registration.url, registration.nextPull)); } - } } diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java new file mode 100644 index 00000000..bba6a402 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.IOException; + +import org.apache.sshd.server.Environment; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.kohsuke.args4j.Argument; + +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.commands.BaseCommand; + +/** + * @author Eric Myhre + * + */ +public abstract class AbstractGitCommand extends BaseCommand { + @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name") + protected String repository; + + protected RepositoryResolver repositoryResolver; + protected ReceivePackFactory receivePackFactory; + protected UploadPackFactory uploadPackFactory; + + protected Repository repo; + + @Override + public void start(final Environment env) { + startThread(new RepositoryCommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + AbstractGitCommand.this.service(); + } + + @Override + public String getRepository() { + return repository; + } + }); + } + + private void service() throws IOException, Failure { + try { + repo = openRepository(); + runImpl(); + } finally { + if (repo != null) { + repo.close(); + } + } + } + + protected abstract void runImpl() throws IOException, Failure; + + protected Repository openRepository() throws Failure { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + repository = repository.replace('\\', '/'); + // ssh://git@thishost/path should always be name="/path" here + // + if (!repository.startsWith("/")) { + throw new Failure(1, "fatal: '" + repository + + "': not starts with / character"); + } + repository = repository.substring(1); + try { + return repositoryResolver.open(ctx.getSession(), repository); + } catch (Exception e) { + throw new Failure(1, "fatal: '" + repository + + "': not a git archive", e); + } + } + + public void setRepositoryResolver( + RepositoryResolver repositoryResolver) { + this.repositoryResolver = repositoryResolver; + } + + public void setReceivePackFactory( + GitblitReceivePackFactory receivePackFactory) { + this.receivePackFactory = receivePackFactory; + } + + public void setUploadPackFactory( + GitblitUploadPackFactory uploadPackFactory) { + this.uploadPackFactory = uploadPackFactory; + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index 056938e9..0c8492f7 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -31,20 +31,9 @@ import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.SessionAware; import org.apache.sshd.server.session.ServerSession; -import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.PacketLineOut; -import org.eclipse.jgit.transport.ReceivePack; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.UploadPack; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.transport.ssh.commands.DispatchCommand; import com.gitblit.utils.WorkQueue; import com.google.common.util.concurrent.Atomics; @@ -57,23 +46,13 @@ import com.google.common.util.concurrent.Atomics; public class SshCommandFactory implements CommandFactory { private static final Logger logger = LoggerFactory .getLogger(SshCommandFactory.class); - private RepositoryResolver repositoryResolver; - - private UploadPackFactory uploadPackFactory; - - private ReceivePackFactory receivePackFactory; private final ScheduledExecutorService startExecutor; private DispatchCommand dispatcher; - public SshCommandFactory(RepositoryResolver repositoryResolver, - UploadPackFactory uploadPackFactory, - ReceivePackFactory receivePackFactory, + public SshCommandFactory( WorkQueue workQueue, DispatchCommand d) { - this.repositoryResolver = repositoryResolver; - this.uploadPackFactory = uploadPackFactory; - this.receivePackFactory = receivePackFactory; this.dispatcher = d; int threads = 2;//cfg.getInt("sshd","commandStartThreads", 2); startExecutor = workQueue.createQueue(threads, "SshCommandStart"); @@ -82,35 +61,34 @@ public class SshCommandFactory implements CommandFactory { @Override public Command createCommand(final String commandLine) { return new Trampoline(commandLine); - /* - if ("git-upload-pack".equals(command)) - return new UploadPackCommand(argument); - if ("git-receive-pack".equals(command)) - return new ReceivePackCommand(argument); - return new NonCommand(); - */ } private class Trampoline implements Command, SessionAware { private final String[] argv; + private ServerSession session; private InputStream in; private OutputStream out; private OutputStream err; private ExitCallback exit; private Environment env; + private String cmdLine; private DispatchCommand cmd; private final AtomicBoolean logged; private final AtomicReference> task; - Trampoline(final String cmdLine) { - argv = split(cmdLine); + Trampoline(String line) { + if (line.startsWith("git-")) { + line = "git " + line; + } + cmdLine = line; + argv = split(line); logged = new AtomicBoolean(); task = Atomics.newReference(); } @Override public void setSession(ServerSession session) { - // TODO Auto-generated method stub + this.session = session; } @Override @@ -148,18 +126,18 @@ public class SshCommandFactory implements CommandFactory { @Override public String toString() { - //return "start (user " + ctx.getSession().getUsername() + ")"; - return "start (user TODO)"; + return "start (user " + session.getUsername() + ")"; } })); } private void onStart() throws IOException { synchronized (this) { - //final Context old = sshScope.set(ctx); + SshContext ctx = new SshContext(session.getAttribute(SshSession.KEY), cmdLine); try { cmd = dispatcher; cmd.setArguments(argv); + cmd.setContext(ctx); cmd.setInputStream(in); cmd.setOutputStream(out); cmd.setErrorStream(err); @@ -178,7 +156,7 @@ public class SshCommandFactory implements CommandFactory { }); cmd.start(env); } finally { - //sshScope.set(old); + ctx = null; } } } @@ -286,101 +264,4 @@ public class SshCommandFactory implements CommandFactory { } return list.toArray(new String[list.size()]); } - - public abstract class RepositoryCommand extends AbstractSshCommand { - protected final String repositoryName; - - public RepositoryCommand(String repositoryName) { - this.repositoryName = repositoryName; - } - - @Override - public void start(Environment env) throws IOException { - Repository db = null; - try { - SshSession client = session.getAttribute(SshSession.KEY); - db = selectRepository(client, repositoryName); - if (db == null) return; - run(client, db); - exit.onExit(0); - } catch (ServiceNotEnabledException e) { - // Ignored. Client cannot use this repository. - } catch (ServiceNotAuthorizedException e) { - // Ignored. Client cannot use this repository. - } finally { - if (db != null) - db.close(); - exit.onExit(1); - } - } - - protected Repository selectRepository(SshSession client, String name) throws IOException { - try { - return openRepository(client, name); - } catch (ServiceMayNotContinueException e) { - // An error when opening the repo means the client is expecting a ref - // advertisement, so use that style of error. - PacketLineOut pktOut = new PacketLineOut(out); - pktOut.writeString("ERR " + e.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - return null; - } - } - - protected Repository openRepository(SshSession client, String name) - throws ServiceMayNotContinueException { - // Assume any attempt to use \ was by a Windows client - // and correct to the more typical / used in Git URIs. - // - name = name.replace('\\', '/'); - - // ssh://git@thishost/path should always be name="/path" here - // - if (!name.startsWith("/")) //$NON-NLS-1$ - return null; - - try { - return repositoryResolver.open(client, name.substring(1)); - } catch (RepositoryNotFoundException e) { - // null signals it "wasn't found", which is all that is suitable - // for the remote client to know. - return null; - } catch (ServiceNotEnabledException e) { - // null signals it "wasn't found", which is all that is suitable - // for the remote client to know. - return null; - } - } - - protected abstract void run(SshSession client, Repository db) - throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException; - } - - public class UploadPackCommand extends RepositoryCommand { - public UploadPackCommand(String repositoryName) { super(repositoryName); } - - @Override - protected void run(SshSession client, Repository db) - throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { - UploadPack up = uploadPackFactory.create(client, db); - up.upload(in, out, null); - } - } - - public class ReceivePackCommand extends RepositoryCommand { - public ReceivePackCommand(String repositoryName) { super(repositoryName); } - - @Override - protected void run(SshSession client, Repository db) - throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { - ReceivePack rp = receivePackFactory.create(client, db); - rp.receive(in, out, null); - } - } - - public static class NonCommand extends AbstractSshCommand { - @Override - public void start(Environment env) { - exit.onExit(127); - } - } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshContext.java b/src/main/java/com/gitblit/transport/ssh/SshContext.java new file mode 100644 index 00000000..b137cb87 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshContext.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +public class SshContext { + + private final SshSession session; + private final String commandLine; + + public SshContext(SshSession session, String commandLine) { + this.session = session; + this.commandLine = commandLine; + } + + public SshSession getSession() { + return session; + } + + public String getCommandLine() { + return commandLine; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index dd4a2d8e..b23ddd58 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -35,6 +35,8 @@ import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.Receive; +import com.gitblit.transport.ssh.commands.Upload; import com.gitblit.transport.ssh.commands.VersionCommand; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; @@ -65,9 +67,6 @@ public class SshDaemon { @SuppressWarnings("unused") private final IGitblit gitblit; - - private final IdGenerator idGenerator; - private final SshServer sshd; /** @@ -77,7 +76,6 @@ public class SshDaemon { */ public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) { this.gitblit = gitblit; - this.idGenerator = idGenerator; IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); @@ -106,15 +104,21 @@ public class SshDaemon { gitblitCmd.registerCommand(CreateRepository.class); gitblitCmd.registerCommand(VersionCommand.class); - DispatchCommand dispatcher = new DispatchCommand(); - dispatcher.registerDispatcher("gitblit", gitblitCmd); + DispatchCommand gitCmd = new DispatchCommand(); + gitCmd.registerCommand(Upload.class); + gitCmd.registerCommand(Receive.class); + + DispatchCommand root = new DispatchCommand(); + root.registerDispatcher("gitblit", gitblitCmd); + root.registerDispatcher("git", gitCmd); + + root.setRepositoryResolver(new RepositoryResolver(gitblit)); + root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); + root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); SshCommandFactory commandFactory = new SshCommandFactory( - new RepositoryResolver(gitblit), - new GitblitUploadPackFactory(gitblit), - new GitblitReceivePackFactory(gitblit), new WorkQueue(idGenerator), - dispatcher); + root); sshd.setCommandFactory(commandFactory); diff --git a/src/main/java/com/gitblit/transport/ssh/SshSession.java b/src/main/java/com/gitblit/transport/ssh/SshSession.java index 9f18a197..ffff8af4 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshSession.java +++ b/src/main/java/com/gitblit/transport/ssh/SshSession.java @@ -36,6 +36,7 @@ public class SshSession { private volatile String username; private volatile String authError; + private volatile String repositoryName; SshSession(int sessionId, SocketAddress peer) { this.sessionId = sessionId; @@ -78,6 +79,14 @@ public class SshSession { authError = error; } + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public String getRepositoryName() { + return repositoryName; + } + /** @return {@code true} if the authentication did not succeed. */ boolean isAuthenticationError() { return authError != null; diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index fd73ccfd..a04c505f 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -33,8 +33,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.transport.ssh.AbstractSshCommand; +import com.gitblit.transport.ssh.SshContext; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.WorkQueue; +import com.gitblit.utils.WorkQueue.CancelableRunnable; import com.gitblit.utils.cli.CmdLineParser; import com.google.common.base.Charsets; import com.google.common.util.concurrent.Atomics; @@ -49,6 +51,9 @@ public abstract class BaseCommand extends AbstractSshCommand { /** Unparsed command line options. */ private String[] argv; + /** Ssh context */ + protected SshContext ctx; + /** The task, as scheduled on a worker thread. */ private final AtomicReference> task; @@ -61,6 +66,10 @@ public abstract class BaseCommand extends AbstractSshCommand { this.executor = w.getDefaultQueue(); } + public void setContext(SshContext ctx) { + this.ctx = ctx; + } + public void setInputStream(final InputStream in) { this.in = in; } @@ -77,7 +86,10 @@ public abstract class BaseCommand extends AbstractSshCommand { this.exit = callback; } - protected void provideStateTo(final Command cmd) { + protected void provideBaseStateTo(final Command cmd) { + if (cmd instanceof BaseCommand) { + ((BaseCommand)cmd).setContext(ctx); + } cmd.setInputStream(in); cmd.setOutputStream(out); cmd.setErrorStream(err); @@ -155,31 +167,25 @@ public abstract class BaseCommand extends AbstractSshCommand { return ""; } - private final class TaskThunk implements com.gitblit.utils.WorkQueue.CancelableRunnable { + private final class TaskThunk implements CancelableRunnable { private final CommandRunnable thunk; private final String taskName; private TaskThunk(final CommandRunnable thunk) { this.thunk = thunk; - // TODO -// StringBuilder m = new StringBuilder("foo"); -// m.append(context.getCommandLine()); -// if (userProvider.get().isIdentifiedUser()) { -// IdentifiedUser u = (IdentifiedUser) userProvider.get(); -// m.append(" (").append(u.getAccount().getUserName()).append(")"); -// } - this.taskName = "foo";//m.toString(); + StringBuilder m = new StringBuilder(); + m.append(ctx.getCommandLine()); + this.taskName = m.toString(); } @Override public void cancel() { synchronized (this) { - //final Context old = sshScope.set(context); try { //onExit(/*STATUS_CANCEL*/); } finally { - //sshScope.set(old); + ctx = null; } } } @@ -190,11 +196,8 @@ public abstract class BaseCommand extends AbstractSshCommand { final Thread thisThread = Thread.currentThread(); final String thisName = thisThread.getName(); int rc = 0; - //final Context old = sshScope.set(context); try { - //context.started = TimeUtil.nowMs(); thisThread.setName("SSH " + taskName); - thunk.run(); out.flush(); @@ -231,6 +234,11 @@ public abstract class BaseCommand extends AbstractSshCommand { } + /** Runnable function which can retrieve a project name related to the task */ + public static interface RepositoryCommandRunnable extends CommandRunnable { + public String getRepository(); + } + /** * Spawn a function into its own thread. *

diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index b6944eaf..597b9ea1 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -27,7 +27,12 @@ import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.kohsuke.args4j.Argument; +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; import com.google.common.base.Strings; @@ -95,11 +100,11 @@ public class DispatchCommand extends BaseCommand { bc.setName(getName() + " " + commandName); } bc.setArguments(args.toArray(new String[args.size()])); - } else if (!args.isEmpty()) { - throw new UnloggedFailure(1, commandName + " does not take arguments"); } - provideStateTo(cmd); + provideBaseStateTo(cmd); + provideGitState(cmd); + reset(); //atomicCmd.set(cmd); cmd.start(env); @@ -136,7 +141,7 @@ public class DispatchCommand extends BaseCommand { } @Override -protected String usage() { + protected String usage() { final StringBuilder usage = new StringBuilder(); usage.append("Available commands"); if (!getName().isEmpty()) { @@ -173,4 +178,39 @@ protected String usage() { usage.append("\n"); return usage.toString(); } + + // This is needed because we are not using provider or + // clazz.newInstance() for DispatchCommand + private void reset() { + args = new ArrayList(); + } + + private void provideGitState(Command cmd) { + if (cmd instanceof AbstractGitCommand) { + AbstractGitCommand a = (AbstractGitCommand) cmd; + a.setRepositoryResolver(repositoryResolver); + a.setUploadPackFactory(gitblitUploadPackFactory); + a.setReceivePackFactory(gitblitReceivePackFactory); + } else if (cmd instanceof DispatchCommand) { + DispatchCommand d = (DispatchCommand)cmd; + d.setRepositoryResolver(repositoryResolver); + d.setUploadPackFactory(gitblitUploadPackFactory); + d.setReceivePackFactory(gitblitReceivePackFactory); + } + } + + private RepositoryResolver repositoryResolver; + public void setRepositoryResolver(RepositoryResolver repositoryResolver) { + this.repositoryResolver = repositoryResolver; + } + + private GitblitUploadPackFactory gitblitUploadPackFactory; + public void setUploadPackFactory(GitblitUploadPackFactory gitblitUploadPackFactory) { + this.gitblitUploadPackFactory = gitblitUploadPackFactory; + } + + private GitblitReceivePackFactory gitblitReceivePackFactory; + public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { + this.gitblitReceivePackFactory = gitblitReceivePackFactory; + } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java new file mode 100644 index 00000000..dd1e8a06 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import org.eclipse.jgit.transport.ReceivePack; + +import com.gitblit.transport.ssh.AbstractGitCommand; +import com.gitblit.transport.ssh.CommandMetaData; + +@CommandMetaData(name = "git-receive-pack", description = "Receive pack") +public class Receive extends AbstractGitCommand { + @Override + protected void runImpl() throws Failure { + try { + ReceivePack rp = receivePackFactory.create(ctx.getSession(), repo); + rp.receive(in, out, null); + } catch (Exception e) { + throw new Failure(1, "fatal: Cannot receive pack: ", e); + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java new file mode 100644 index 00000000..d6c3f961 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import javax.inject.Inject; + +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; + +import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.AbstractGitCommand; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.SshSession; + +@CommandMetaData(name = "git-upload-pack", description = "Upload pack") +public class Upload extends AbstractGitCommand { + @Override + protected void runImpl() throws Failure { + try { + UploadPack up = uploadPackFactory.create(ctx.getSession(), repo); + up.upload(in, out, null); + } catch (Exception e) { + throw new Failure(1, "fatal: Cannot upload pack: ", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java index baae6a2c..fc3e01b3 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java @@ -29,7 +29,7 @@ public class VersionCommand extends SshCommand { @Override public void run() { - stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(), + stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(), verbose)); } } -- cgit v1.2.3 From 31f477050f223b975bce4862a6aa415ee3045857 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 19:10:20 -0500 Subject: Move Git daemon into different package --- src/main/java/com/gitblit/git/GitDaemon.java | 333 -------------------- src/main/java/com/gitblit/git/GitDaemonClient.java | 131 -------- .../java/com/gitblit/git/GitDaemonService.java | 166 ---------- .../com/gitblit/git/GitblitReceivePackFactory.java | 1 + .../com/gitblit/git/GitblitUploadPackFactory.java | 4 +- .../java/com/gitblit/git/RepositoryResolver.java | 1 + .../java/com/gitblit/manager/ServicesManager.java | 2 +- .../java/com/gitblit/transport/git/GitDaemon.java | 336 +++++++++++++++++++++ .../com/gitblit/transport/git/GitDaemonClient.java | 131 ++++++++ .../gitblit/transport/git/GitDaemonService.java | 166 ++++++++++ .../java/com/gitblit/transport/ssh/SshDaemon.java | 2 +- 11 files changed, 640 insertions(+), 633 deletions(-) delete mode 100644 src/main/java/com/gitblit/git/GitDaemon.java delete mode 100644 src/main/java/com/gitblit/git/GitDaemonClient.java delete mode 100644 src/main/java/com/gitblit/git/GitDaemonService.java create mode 100644 src/main/java/com/gitblit/transport/git/GitDaemon.java create mode 100644 src/main/java/com/gitblit/transport/git/GitDaemonClient.java create mode 100644 src/main/java/com/gitblit/transport/git/GitDaemonService.java (limited to 'src') diff --git a/src/main/java/com/gitblit/git/GitDaemon.java b/src/main/java/com/gitblit/git/GitDaemon.java deleted file mode 100644 index d026b5ed..00000000 --- a/src/main/java/com/gitblit/git/GitDaemon.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright (C) 2013 gitblit.com - * Copyright (C) 2008-2009, Google Inc. - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.gitblit.git; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketAddress; -import java.text.MessageFormat; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.internal.JGitText; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ReceivePack; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.UploadPack; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.IStoredSettings; -import com.gitblit.Keys; -import com.gitblit.manager.IGitblit; -import com.gitblit.utils.StringUtils; - -/** - * Gitblit's Git Daemon ignores any and all per-repository daemon settings and - * integrates into Gitblit's security model. - * - * @author James Moger - * - */ -public class GitDaemon { - - private final Logger logger = LoggerFactory.getLogger(GitDaemon.class); - - /** 9418: IANA assigned port number for Git. */ - public static final int DEFAULT_PORT = 9418; - - private static final int BACKLOG = 5; - - private InetSocketAddress myAddress; - - private final GitDaemonService[] services; - - private final ThreadGroup processors; - - private AtomicBoolean run; - - private ServerSocket acceptSocket; - - private Thread acceptThread; - - private int timeout; - - private RepositoryResolver repositoryResolver; - - private UploadPackFactory uploadPackFactory; - - private ReceivePackFactory receivePackFactory; - - public GitDaemon(IGitblit gitblit) { - - IStoredSettings settings = gitblit.getSettings(); - int port = settings.getInteger(Keys.git.daemonPort, 0); - String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost"); - - if (StringUtils.isEmpty(bindInterface)) { - myAddress = new InetSocketAddress(port); - } else { - myAddress = new InetSocketAddress(bindInterface, port); - } - - repositoryResolver = new RepositoryResolver(gitblit); - uploadPackFactory = new GitblitUploadPackFactory(gitblit); - receivePackFactory = new GitblitReceivePackFactory(gitblit); - - run = new AtomicBoolean(false); - processors = new ThreadGroup("Git-Daemon"); - services = new GitDaemonService[] { new GitDaemonService("upload-pack", "uploadpack") { - { - setEnabled(true); - setOverridable(false); - } - - @Override - protected void execute(final GitDaemonClient dc, final Repository db) - throws IOException, ServiceNotEnabledException, - ServiceNotAuthorizedException { - UploadPack up = uploadPackFactory.create(dc, db); - InputStream in = dc.getInputStream(); - OutputStream out = dc.getOutputStream(); - up.upload(in, out, null); - } - }, new GitDaemonService("receive-pack", "receivepack") { - { - setEnabled(true); - setOverridable(false); - } - - @Override - protected void execute(final GitDaemonClient dc, final Repository db) - throws IOException, ServiceNotEnabledException, - ServiceNotAuthorizedException { - ReceivePack rp = receivePackFactory.create(dc, db); - InputStream in = dc.getInputStream(); - OutputStream out = dc.getOutputStream(); - rp.receive(in, out, null); - } - } }; - } - - public int getPort() { - return myAddress.getPort(); - } - - public String formatUrl(String servername, String repository) { - if (getPort() == 9418) { - // standard port - return MessageFormat.format("git://{0}/{1}", servername, repository); - } else { - // non-standard port - return MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, getPort(), repository); - } - } - - /** @return timeout (in seconds) before aborting an IO operation. */ - public int getTimeout() { - return timeout; - } - - /** - * Set the timeout before willing to abort an IO call. - * - * @param seconds - * number of seconds to wait (with no data transfer occurring) - * before aborting an IO read or write operation with the - * connected client. - */ - public void setTimeout(final int seconds) { - timeout = seconds; - } - - /** - * Start this daemon on a background thread. - * - * @throws IOException - * the server socket could not be opened. - * @throws IllegalStateException - * the daemon is already running. - */ - public synchronized void start() throws IOException { - if (acceptThread != null) - throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); - - final ServerSocket listenSock = new ServerSocket(myAddress != null ? myAddress.getPort() - : 0, BACKLOG, myAddress != null ? myAddress.getAddress() : null); - myAddress = (InetSocketAddress) listenSock.getLocalSocketAddress(); - - run.set(true); - acceptSocket = listenSock; - acceptThread = new Thread(processors, "Git-Daemon-Accept") { - @Override - public void run() { - while (isRunning()) { - try { - startClient(listenSock.accept()); - } catch (InterruptedIOException e) { - // Test again to see if we should keep accepting. - } catch (IOException e) { - break; - } - } - - try { - listenSock.close(); - } catch (IOException err) { - // - } finally { - acceptSocket = null; - } - - } - }; - acceptThread.start(); - - logger.info(MessageFormat.format("Git Daemon is listening on {0}:{1,number,0}", myAddress.getAddress().getHostAddress(), myAddress.getPort())); - } - - /** @return true if this daemon is receiving connections. */ - public boolean isRunning() { - return run.get(); - } - - /** Stop this daemon. */ - public synchronized void stop() { - if (isRunning() && acceptThread != null) { - run.set(false); - logger.info("Git Daemon stopping..."); - try { - // close the accept socket - // this throws a SocketException in the accept thread - acceptSocket.close(); - } catch (IOException e1) { - } - try { - // join the accept thread - acceptThread.join(); - logger.info("Git Daemon stopped."); - } catch (InterruptedException e) { - logger.error("Accept thread join interrupted", e); - } finally { - acceptThread = null; - } - } - } - - private void startClient(final Socket s) { - final GitDaemonClient dc = new GitDaemonClient(this); - - final SocketAddress peer = s.getRemoteSocketAddress(); - if (peer instanceof InetSocketAddress) - dc.setRemoteAddress(((InetSocketAddress) peer).getAddress()); - - new Thread(processors, "Git-Daemon-Client " + peer.toString()) { - @Override - public void run() { - try { - dc.execute(s); - } catch (ServiceNotEnabledException e) { - // Ignored. Client cannot use this repository. - } catch (ServiceNotAuthorizedException e) { - // Ignored. Client cannot use this repository. - } catch (IOException e) { - // Ignore unexpected IO exceptions from clients - } finally { - try { - s.getInputStream().close(); - } catch (IOException e) { - // Ignore close exceptions - } - try { - s.getOutputStream().close(); - } catch (IOException e) { - // Ignore close exceptions - } - } - } - }.start(); - } - - synchronized GitDaemonService matchService(final String cmd) { - for (final GitDaemonService d : services) { - if (d.handles(cmd)) - return d; - } - return null; - } - - Repository openRepository(GitDaemonClient client, String name) - throws ServiceMayNotContinueException { - // Assume any attempt to use \ was by a Windows client - // and correct to the more typical / used in Git URIs. - // - name = name.replace('\\', '/'); - - // git://thishost/path should always be name="/path" here - // - if (!name.startsWith("/")) //$NON-NLS-1$ - return null; - - try { - return repositoryResolver.open(client, name.substring(1)); - } catch (RepositoryNotFoundException e) { - // null signals it "wasn't found", which is all that is suitable - // for the remote client to know. - return null; - } catch (ServiceNotEnabledException e) { - // null signals it "wasn't found", which is all that is suitable - // for the remote client to know. - return null; - } - } -} diff --git a/src/main/java/com/gitblit/git/GitDaemonClient.java b/src/main/java/com/gitblit/git/GitDaemonClient.java deleted file mode 100644 index 8d8cac6d..00000000 --- a/src/main/java/com/gitblit/git/GitDaemonClient.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.gitblit.git; - -/* - * Copyright (C) 2008-2009, Google Inc. - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.Socket; - -import org.eclipse.jgit.transport.Daemon; -import org.eclipse.jgit.transport.PacketLineIn; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; -import org.eclipse.jgit.util.io.SafeBufferedOutputStream; - -/** Active network client of {@link Daemon}. */ -public class GitDaemonClient { - private final GitDaemon daemon; - - private InetAddress peer; - - private InputStream rawIn; - - private OutputStream rawOut; - - private String repositoryName; - - GitDaemonClient(final GitDaemon d) { - daemon = d; - } - - void setRemoteAddress(final InetAddress ia) { - peer = ia; - } - - /** @return the daemon which spawned this client. */ - public GitDaemon getDaemon() { - return daemon; - } - - /** @return Internet address of the remote client. */ - public InetAddress getRemoteAddress() { - return peer; - } - - /** @return input stream to read from the connected client. */ - public InputStream getInputStream() { - return rawIn; - } - - /** @return output stream to send data to the connected client. */ - public OutputStream getOutputStream() { - return rawOut; - } - - public void setRepositoryName(String repositoryName) { - this.repositoryName = repositoryName; - } - - /** @return the name of the requested repository. */ - public String getRepositoryName() { - return repositoryName; - } - - void execute(final Socket sock) throws IOException, - ServiceNotEnabledException, ServiceNotAuthorizedException { - rawIn = new BufferedInputStream(sock.getInputStream()); - rawOut = new SafeBufferedOutputStream(sock.getOutputStream()); - - if (0 < daemon.getTimeout()) - sock.setSoTimeout(daemon.getTimeout() * 1000); - String cmd = new PacketLineIn(rawIn).readStringRaw(); - final int nul = cmd.indexOf('\0'); - if (nul >= 0) { - // Newer clients hide a "host" header behind this byte. - // Currently we don't use it for anything, so we ignore - // this portion of the command. - // - cmd = cmd.substring(0, nul); - } - - final GitDaemonService srv = getDaemon().matchService(cmd); - if (srv == null) - return; - sock.setSoTimeout(0); - srv.execute(this, cmd); - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/git/GitDaemonService.java b/src/main/java/com/gitblit/git/GitDaemonService.java deleted file mode 100644 index 8dee7d0b..00000000 --- a/src/main/java/com/gitblit/git/GitDaemonService.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.gitblit.git; - -/* - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2009, Robin Rosenberg - * and other copyright owners as documented in the project's IP log. - * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import java.io.IOException; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.Config.SectionParser; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.Daemon; -import org.eclipse.jgit.transport.PacketLineOut; -import org.eclipse.jgit.transport.ServiceMayNotContinueException; -import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; - -/** A service exposed by {@link Daemon} over anonymous git://. */ -public abstract class GitDaemonService { - private final String command; - - private final SectionParser configKey; - - private boolean enabled; - - private boolean overridable; - - GitDaemonService(final String cmdName, final String cfgName) { - command = cmdName.startsWith("git-") ? cmdName : "git-" + cmdName; //$NON-NLS-1$ //$NON-NLS-2$ - configKey = new SectionParser() { - @Override - public ServiceConfig parse(final Config cfg) { - return new ServiceConfig(GitDaemonService.this, cfg, cfgName); - } - }; - overridable = true; - } - - private static class ServiceConfig { - final boolean enabled; - - ServiceConfig(final GitDaemonService service, final Config cfg, - final String name) { - enabled = cfg.getBoolean("daemon", name, service.isEnabled()); //$NON-NLS-1$ - } - } - - /** @return is this service enabled for invocation? */ - public boolean isEnabled() { - return enabled; - } - - /** - * @param on - * true to allow this service to be used; false to deny it. - */ - public void setEnabled(final boolean on) { - enabled = on; - } - - /** @return can this service be configured in the repository config file? */ - public boolean isOverridable() { - return overridable; - } - - /** - * @param on - * true to permit repositories to override this service's enabled - * state with the daemon.servicename config setting. - */ - public void setOverridable(final boolean on) { - overridable = on; - } - - /** @return name of the command requested by clients. */ - public String getCommandName() { - return command; - } - - /** - * Determine if this service can handle the requested command. - * - * @param commandLine - * input line from the client. - * @return true if this command can accept the given command line. - */ - public boolean handles(final String commandLine) { - return command.length() + 1 < commandLine.length() - && commandLine.charAt(command.length()) == ' ' - && commandLine.startsWith(command); - } - - void execute(final GitDaemonClient client, final String commandLine) - throws IOException, ServiceNotEnabledException, - ServiceNotAuthorizedException { - final String name = commandLine.substring(command.length() + 1); - Repository db; - try { - db = client.getDaemon().openRepository(client, name); - } catch (ServiceMayNotContinueException e) { - // An error when opening the repo means the client is expecting a ref - // advertisement, so use that style of error. - PacketLineOut pktOut = new PacketLineOut(client.getOutputStream()); - pktOut.writeString("ERR " + e.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - db = null; - } - if (db == null) - return; - try { - if (isEnabledFor(db)) - execute(client, db); - } finally { - db.close(); - } - } - - private boolean isEnabledFor(final Repository db) { - if (isOverridable()) - return db.getConfig().get(configKey).enabled; - return isEnabled(); - } - - abstract void execute(GitDaemonClient client, Repository db) - throws IOException, ServiceNotEnabledException, - ServiceNotAuthorizedException; -} diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java index 9911258c..af5a8381 100644 --- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java @@ -31,6 +31,7 @@ import com.gitblit.Keys; import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; +import com.gitblit.transport.git.GitDaemonClient; import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.StringUtils; diff --git a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java index d4e3ca15..39ad0724 100644 --- a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java @@ -25,6 +25,7 @@ import org.eclipse.jgit.transport.resolver.UploadPackFactory; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; +import com.gitblit.transport.git.GitDaemonClient; /** * The upload pack factory creates an upload pack which controls what refs are @@ -51,7 +52,8 @@ public class GitblitUploadPackFactory implements UploadPackFactory { if (req instanceof HttpServletRequest) { // http/https request may or may not be authenticated - user = authenticationManager.authenticate((HttpServletRequest) req); + HttpServletRequest client = (HttpServletRequest) req; + user = authenticationManager.authenticate(client); if (user == null) { user = UserModel.ANONYMOUS; } diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java index c859f6f6..08048195 100644 --- a/src/main/java/com/gitblit/git/RepositoryResolver.java +++ b/src/main/java/com/gitblit/git/RepositoryResolver.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; +import com.gitblit.transport.git.GitDaemonClient; import com.gitblit.transport.ssh.SshSession; /** diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index 11083be3..f75c6d14 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -37,11 +37,11 @@ import com.gitblit.Keys; import com.gitblit.fanout.FanoutNioService; import com.gitblit.fanout.FanoutService; import com.gitblit.fanout.FanoutSocketService; -import com.gitblit.git.GitDaemon; import com.gitblit.models.FederationModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.service.FederationPullService; +import com.gitblit.transport.git.GitDaemon; import com.gitblit.transport.ssh.SshDaemon; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; diff --git a/src/main/java/com/gitblit/transport/git/GitDaemon.java b/src/main/java/com/gitblit/transport/git/GitDaemon.java new file mode 100644 index 00000000..6581ad87 --- /dev/null +++ b/src/main/java/com/gitblit/transport/git/GitDaemon.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2013 gitblit.com + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.gitblit.transport.git; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.text.MessageFormat; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; +import org.eclipse.jgit.transport.UploadPack; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.manager.IGitblit; +import com.gitblit.utils.StringUtils; + +/** + * Gitblit's Git Daemon ignores any and all per-repository daemon settings and + * integrates into Gitblit's security model. + * + * @author James Moger + * + */ +public class GitDaemon { + + private final Logger logger = LoggerFactory.getLogger(GitDaemon.class); + + /** 9418: IANA assigned port number for Git. */ + public static final int DEFAULT_PORT = 9418; + + private static final int BACKLOG = 5; + + private InetSocketAddress myAddress; + + private final GitDaemonService[] services; + + private final ThreadGroup processors; + + private AtomicBoolean run; + + private ServerSocket acceptSocket; + + private Thread acceptThread; + + private int timeout; + + private RepositoryResolver repositoryResolver; + + private UploadPackFactory uploadPackFactory; + + private ReceivePackFactory receivePackFactory; + + public GitDaemon(IGitblit gitblit) { + + IStoredSettings settings = gitblit.getSettings(); + int port = settings.getInteger(Keys.git.daemonPort, 0); + String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost"); + + if (StringUtils.isEmpty(bindInterface)) { + myAddress = new InetSocketAddress(port); + } else { + myAddress = new InetSocketAddress(bindInterface, port); + } + + repositoryResolver = new RepositoryResolver(gitblit); + uploadPackFactory = new GitblitUploadPackFactory(gitblit); + receivePackFactory = new GitblitReceivePackFactory(gitblit); + + run = new AtomicBoolean(false); + processors = new ThreadGroup("Git-Daemon"); + services = new GitDaemonService[] { new GitDaemonService("upload-pack", "uploadpack") { + { + setEnabled(true); + setOverridable(false); + } + + @Override + protected void execute(final GitDaemonClient dc, final Repository db) + throws IOException, ServiceNotEnabledException, + ServiceNotAuthorizedException { + UploadPack up = uploadPackFactory.create(dc, db); + InputStream in = dc.getInputStream(); + OutputStream out = dc.getOutputStream(); + up.upload(in, out, null); + } + }, new GitDaemonService("receive-pack", "receivepack") { + { + setEnabled(true); + setOverridable(false); + } + + @Override + protected void execute(final GitDaemonClient dc, final Repository db) + throws IOException, ServiceNotEnabledException, + ServiceNotAuthorizedException { + ReceivePack rp = receivePackFactory.create(dc, db); + InputStream in = dc.getInputStream(); + OutputStream out = dc.getOutputStream(); + rp.receive(in, out, null); + } + } }; + } + + public int getPort() { + return myAddress.getPort(); + } + + public String formatUrl(String servername, String repository) { + if (getPort() == 9418) { + // standard port + return MessageFormat.format("git://{0}/{1}", servername, repository); + } else { + // non-standard port + return MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, getPort(), repository); + } + } + + /** @return timeout (in seconds) before aborting an IO operation. */ + public int getTimeout() { + return timeout; + } + + /** + * Set the timeout before willing to abort an IO call. + * + * @param seconds + * number of seconds to wait (with no data transfer occurring) + * before aborting an IO read or write operation with the + * connected client. + */ + public void setTimeout(final int seconds) { + timeout = seconds; + } + + /** + * Start this daemon on a background thread. + * + * @throws IOException + * the server socket could not be opened. + * @throws IllegalStateException + * the daemon is already running. + */ + public synchronized void start() throws IOException { + if (acceptThread != null) + throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); + + final ServerSocket listenSock = new ServerSocket(myAddress != null ? myAddress.getPort() + : 0, BACKLOG, myAddress != null ? myAddress.getAddress() : null); + myAddress = (InetSocketAddress) listenSock.getLocalSocketAddress(); + + run.set(true); + acceptSocket = listenSock; + acceptThread = new Thread(processors, "Git-Daemon-Accept") { + @Override + public void run() { + while (isRunning()) { + try { + startClient(listenSock.accept()); + } catch (InterruptedIOException e) { + // Test again to see if we should keep accepting. + } catch (IOException e) { + break; + } + } + + try { + listenSock.close(); + } catch (IOException err) { + // + } finally { + acceptSocket = null; + } + + } + }; + acceptThread.start(); + + logger.info(MessageFormat.format("Git Daemon is listening on {0}:{1,number,0}", myAddress.getAddress().getHostAddress(), myAddress.getPort())); + } + + /** @return true if this daemon is receiving connections. */ + public boolean isRunning() { + return run.get(); + } + + /** Stop this daemon. */ + public synchronized void stop() { + if (isRunning() && acceptThread != null) { + run.set(false); + logger.info("Git Daemon stopping..."); + try { + // close the accept socket + // this throws a SocketException in the accept thread + acceptSocket.close(); + } catch (IOException e1) { + } + try { + // join the accept thread + acceptThread.join(); + logger.info("Git Daemon stopped."); + } catch (InterruptedException e) { + logger.error("Accept thread join interrupted", e); + } finally { + acceptThread = null; + } + } + } + + private void startClient(final Socket s) { + final GitDaemonClient dc = new GitDaemonClient(this); + + final SocketAddress peer = s.getRemoteSocketAddress(); + if (peer instanceof InetSocketAddress) + dc.setRemoteAddress(((InetSocketAddress) peer).getAddress()); + + new Thread(processors, "Git-Daemon-Client " + peer.toString()) { + @Override + public void run() { + try { + dc.execute(s); + } catch (ServiceNotEnabledException e) { + // Ignored. Client cannot use this repository. + } catch (ServiceNotAuthorizedException e) { + // Ignored. Client cannot use this repository. + } catch (IOException e) { + // Ignore unexpected IO exceptions from clients + } finally { + try { + s.getInputStream().close(); + } catch (IOException e) { + // Ignore close exceptions + } + try { + s.getOutputStream().close(); + } catch (IOException e) { + // Ignore close exceptions + } + } + } + }.start(); + } + + synchronized GitDaemonService matchService(final String cmd) { + for (final GitDaemonService d : services) { + if (d.handles(cmd)) + return d; + } + return null; + } + + Repository openRepository(GitDaemonClient client, String name) + throws ServiceMayNotContinueException { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + name = name.replace('\\', '/'); + + // git://thishost/path should always be name="/path" here + // + if (!name.startsWith("/")) //$NON-NLS-1$ + return null; + + try { + return repositoryResolver.open(client, name.substring(1)); + } catch (RepositoryNotFoundException e) { + // null signals it "wasn't found", which is all that is suitable + // for the remote client to know. + return null; + } catch (ServiceNotEnabledException e) { + // null signals it "wasn't found", which is all that is suitable + // for the remote client to know. + return null; + } + } +} diff --git a/src/main/java/com/gitblit/transport/git/GitDaemonClient.java b/src/main/java/com/gitblit/transport/git/GitDaemonClient.java new file mode 100644 index 00000000..bc3d4cf7 --- /dev/null +++ b/src/main/java/com/gitblit/transport/git/GitDaemonClient.java @@ -0,0 +1,131 @@ +package com.gitblit.transport.git; + +/* + * Copyright (C) 2008-2009, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; + +import org.eclipse.jgit.transport.Daemon; +import org.eclipse.jgit.transport.PacketLineIn; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.util.io.SafeBufferedOutputStream; + +/** Active network client of {@link Daemon}. */ +public class GitDaemonClient { + private final GitDaemon daemon; + + private InetAddress peer; + + private InputStream rawIn; + + private OutputStream rawOut; + + private String repositoryName; + + GitDaemonClient(final GitDaemon d) { + daemon = d; + } + + void setRemoteAddress(final InetAddress ia) { + peer = ia; + } + + /** @return the daemon which spawned this client. */ + public GitDaemon getDaemon() { + return daemon; + } + + /** @return Internet address of the remote client. */ + public InetAddress getRemoteAddress() { + return peer; + } + + /** @return input stream to read from the connected client. */ + public InputStream getInputStream() { + return rawIn; + } + + /** @return output stream to send data to the connected client. */ + public OutputStream getOutputStream() { + return rawOut; + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + /** @return the name of the requested repository. */ + public String getRepositoryName() { + return repositoryName; + } + + void execute(final Socket sock) throws IOException, + ServiceNotEnabledException, ServiceNotAuthorizedException { + rawIn = new BufferedInputStream(sock.getInputStream()); + rawOut = new SafeBufferedOutputStream(sock.getOutputStream()); + + if (0 < daemon.getTimeout()) + sock.setSoTimeout(daemon.getTimeout() * 1000); + String cmd = new PacketLineIn(rawIn).readStringRaw(); + final int nul = cmd.indexOf('\0'); + if (nul >= 0) { + // Newer clients hide a "host" header behind this byte. + // Currently we don't use it for anything, so we ignore + // this portion of the command. + // + cmd = cmd.substring(0, nul); + } + + final GitDaemonService srv = getDaemon().matchService(cmd); + if (srv == null) + return; + sock.setSoTimeout(0); + srv.execute(this, cmd); + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/git/GitDaemonService.java b/src/main/java/com/gitblit/transport/git/GitDaemonService.java new file mode 100644 index 00000000..989b2b4c --- /dev/null +++ b/src/main/java/com/gitblit/transport/git/GitDaemonService.java @@ -0,0 +1,166 @@ +package com.gitblit.transport.git; + +/* + * Copyright (C) 2008-2009, Google Inc. + * Copyright (C) 2009, Robin Rosenberg + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import java.io.IOException; + +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Config.SectionParser; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.Daemon; +import org.eclipse.jgit.transport.PacketLineOut; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; + +/** A service exposed by {@link Daemon} over anonymous git://. */ +public abstract class GitDaemonService { + private final String command; + + private final SectionParser configKey; + + private boolean enabled; + + private boolean overridable; + + GitDaemonService(final String cmdName, final String cfgName) { + command = cmdName.startsWith("git-") ? cmdName : "git-" + cmdName; //$NON-NLS-1$ //$NON-NLS-2$ + configKey = new SectionParser() { + @Override + public ServiceConfig parse(final Config cfg) { + return new ServiceConfig(GitDaemonService.this, cfg, cfgName); + } + }; + overridable = true; + } + + private static class ServiceConfig { + final boolean enabled; + + ServiceConfig(final GitDaemonService service, final Config cfg, + final String name) { + enabled = cfg.getBoolean("daemon", name, service.isEnabled()); //$NON-NLS-1$ + } + } + + /** @return is this service enabled for invocation? */ + public boolean isEnabled() { + return enabled; + } + + /** + * @param on + * true to allow this service to be used; false to deny it. + */ + public void setEnabled(final boolean on) { + enabled = on; + } + + /** @return can this service be configured in the repository config file? */ + public boolean isOverridable() { + return overridable; + } + + /** + * @param on + * true to permit repositories to override this service's enabled + * state with the daemon.servicename config setting. + */ + public void setOverridable(final boolean on) { + overridable = on; + } + + /** @return name of the command requested by clients. */ + public String getCommandName() { + return command; + } + + /** + * Determine if this service can handle the requested command. + * + * @param commandLine + * input line from the client. + * @return true if this command can accept the given command line. + */ + public boolean handles(final String commandLine) { + return command.length() + 1 < commandLine.length() + && commandLine.charAt(command.length()) == ' ' + && commandLine.startsWith(command); + } + + void execute(final GitDaemonClient client, final String commandLine) + throws IOException, ServiceNotEnabledException, + ServiceNotAuthorizedException { + final String name = commandLine.substring(command.length() + 1); + Repository db; + try { + db = client.getDaemon().openRepository(client, name); + } catch (ServiceMayNotContinueException e) { + // An error when opening the repo means the client is expecting a ref + // advertisement, so use that style of error. + PacketLineOut pktOut = new PacketLineOut(client.getOutputStream()); + pktOut.writeString("ERR " + e.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + db = null; + } + if (db == null) + return; + try { + if (isEnabledFor(db)) + execute(client, db); + } finally { + db.close(); + } + } + + private boolean isEnabledFor(final Repository db) { + if (isOverridable()) + return db.getConfig().get(configKey).enabled; + return isEnabled(); + } + + abstract void execute(GitDaemonClient client, Repository db) + throws IOException, ServiceNotEnabledException, + ServiceNotAuthorizedException; +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index b23ddd58..cc938bc1 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -44,7 +44,7 @@ import com.gitblit.utils.WorkQueue; /** * Manager for the ssh transport. Roughly analogous to the - * {@link com.gitblit.git.GitDaemon} class. + * {@link com.gitblit.transport.git.GitDaemon} class. * * @author Eric Myhre * -- cgit v1.2.3 From 85fbc79605d886b2b4ec02aa8cfb5b8b05436576 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 19:10:53 -0500 Subject: Authenticate SSH upload pack --- src/main/java/com/gitblit/git/GitblitUploadPackFactory.java | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src') diff --git a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java index 39ad0724..a72d4ad9 100644 --- a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java @@ -26,6 +26,7 @@ import org.eclipse.jgit.transport.resolver.UploadPackFactory; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; import com.gitblit.transport.git.GitDaemonClient; +import com.gitblit.transport.ssh.SshSession; /** * The upload pack factory creates an upload pack which controls what refs are @@ -62,6 +63,13 @@ public class GitblitUploadPackFactory implements UploadPackFactory { GitDaemonClient client = (GitDaemonClient) req; // set timeout from Git daemon timeout = client.getDaemon().getTimeout(); + } else if (req instanceof SshSession) { + // SSH request is always authenticated + SshSession client = (SshSession) req; + user = authenticationManager.authenticate(client); + if (user == null) { + throw new ServiceNotAuthorizedException(); + } } UploadPack up = new UploadPack(db); -- cgit v1.2.3 From 4ccdfe90b73c140b330b9f7838d6cb580c98229a Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 20:34:55 -0500 Subject: Refer to receive pack client requests as 'client' --- .../com/gitblit/git/GitblitReceivePackFactory.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java index af5a8381..b928d851 100644 --- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java @@ -69,13 +69,13 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { if (req instanceof HttpServletRequest) { // http/https request may or may not be authenticated - HttpServletRequest request = (HttpServletRequest) req; - repositoryName = request.getAttribute("gitblitRepositoryName").toString(); - origin = request.getRemoteHost(); - gitblitUrl = HttpUtils.getGitblitURL(request); + HttpServletRequest client = (HttpServletRequest) req; + repositoryName = client.getAttribute("gitblitRepositoryName").toString(); + origin = client.getRemoteHost(); + gitblitUrl = HttpUtils.getGitblitURL(client); // determine pushing user - String username = request.getRemoteUser(); + String username = client.getRemoteUser(); if (!StringUtils.isEmpty(username)) { UserModel u = gitblit.getUserModel(username); if (u != null) { @@ -92,10 +92,10 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { timeout = client.getDaemon().getTimeout(); } else if (req instanceof SshSession) { // SSH request is always authenticated - SshSession s = (SshSession) req; - repositoryName = s.getRepositoryName(); - origin = s.getRemoteAddress().toString(); - String username = s.getRemoteUser(); + SshSession client = (SshSession) req; + repositoryName = client.getRepositoryName(); + origin = client.getRemoteAddress().toString(); + String username = client.getRemoteUser(); user = gitblit.getUserModel(username); } -- cgit v1.2.3 From 860d2ca577520850a705298a2b19a0de0459b82e Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 19:43:15 -0500 Subject: Establish ssh keys folder, support multiple keys, revise key authenticator --- src/main/distrib/data/gitblit.properties | 5 + .../com/gitblit/manager/AuthenticationManager.java | 2 +- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 152 ++++++++++----------- .../gitblit/transport/ssh/SshKeyCacheEntry.java | 26 ---- 4 files changed, 82 insertions(+), 103 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 5bc28fd6..fbde84a7 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -110,6 +110,11 @@ git.sshPort = 29418 # RESTART REQUIRED git.sshBindInterface = localhost +# Directory for storing user SSH keys. +# +# SINCE 1.5.0 +git.sshKeysFolder= ${baseFolder}/ssh + # Allow push/pull over http/https with JGit servlet. # If you do NOT want to allow Git clients to clone/push to Gitblit set this # to false. You might want to do this if you are only using ssh:// or git://. diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java index 47425ce7..658c2890 100644 --- a/src/main/java/com/gitblit/manager/AuthenticationManager.java +++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java @@ -304,7 +304,7 @@ public class AuthenticationManager implements IAuthenticationManager { UserModel user = userManager.getUserModel(username); if (user != null) { // existing user - logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}", + logger.debug(MessageFormat.format("{0} authenticated by SSH key from {1}", user.username, sshSession.getRemoteAddress())); return validateAuthentication(user, AuthenticationType.SSH); } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index 4ab20f33..4cda268e 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -18,8 +18,11 @@ package com.gitblit.transport.ssh; import java.io.File; import java.io.IOException; import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Base64; import org.apache.sshd.common.util.Buffer; @@ -27,12 +30,13 @@ import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; import org.eclipse.jgit.lib.Constants; +import com.gitblit.Keys; import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; import com.google.common.base.Charsets; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.cache.Weigher; import com.google.common.io.Files; /** @@ -42,84 +46,80 @@ import com.google.common.io.Files; */ public class SshKeyAuthenticator implements PublickeyAuthenticator { - protected final IGitblit gitblit; + protected final IGitblit gitblit; - LoadingCache sshKeyCache = CacheBuilder - .newBuilder().maximumWeight(2 << 20).weigher(new SshKeyCacheWeigher()) - .build(new CacheLoader() { - public SshKeyCacheEntry load(String key) throws Exception { - return loadKey(key); - } + LoadingCache> sshKeyCache = CacheBuilder + .newBuilder(). + expireAfterAccess(15, TimeUnit.MINUTES). + maximumSize(100) + .build(new CacheLoader>() { + public List load(String username) { + try { + File dir = gitblit.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh"); + dir.mkdirs(); + File keys = new File(dir, username + ".keys"); + if (!keys.exists()) { + return null; + } + if (keys.exists()) { + String str = Files.toString(keys, Charsets.ISO_8859_1); + String [] entries = str.split("\n"); + List list = new ArrayList(); + for (String entry : entries) { + final String[] parts = entry.split(" "); + final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1])); + list.add(new Buffer(bin).getRawPublicKey()); + } + + if (list.isEmpty()) { + return null; + } + return list; + } + } catch (IOException e) { + throw new RuntimeException("Canot read public key", e); + } + return null; + } + }); - private SshKeyCacheEntry loadKey(String key) { - try { - // TODO(davido): retrieve absolute path to public key directory: - //String dir = gitblit.getSettings().getString("public_key_dir", "data/ssh"); - String dir = "/tmp/"; - // Expect public key file name in form: in - File file = new File(dir + key + ".pub"); - String str = Files.toString(file, Charsets.ISO_8859_1); - final String[] parts = str.split(" "); - final byte[] bin = - Base64.decodeBase64(Constants.encodeASCII(parts[1])); - return new SshKeyCacheEntry(key, new Buffer(bin).getRawPublicKey()); - } catch (IOException e) { - throw new RuntimeException("Canot read public key", e); - } - } - }); + public SshKeyAuthenticator(IGitblit gitblit) { + this.gitblit = gitblit; + } - public SshKeyAuthenticator(IGitblit gitblit) { - this.gitblit = gitblit; - } + @Override + public boolean authenticate(String username, final PublicKey suppliedKey, + final ServerSession session) { + final SshSession sd = session.getAttribute(SshSession.KEY); - @Override - public boolean authenticate(String username, final PublicKey suppliedKey, - final ServerSession session) { - final SshSession sd = session.getAttribute(SshSession.KEY); + username = username.toLowerCase(Locale.US); + try { + List keys = sshKeyCache.get(username); + if (keys == null || keys.isEmpty()) { + sd.authenticationError(username, "no-matching-key"); + return false; + } + for (PublicKey key : keys) { + if (key.equals(suppliedKey)) { + return validate(username, sd); + } + } + return false; + } catch (ExecutionException e) { + sd.authenticationError(username, "user-not-found"); + return false; + } + } - // if (config.getBoolean("auth", "userNameToLowerCase", false)) { - username = username.toLowerCase(Locale.US); - // } - try { - // TODO: allow multiple public keys per user - SshKeyCacheEntry key = sshKeyCache.get(username); - if (key == null) { - sd.authenticationError(username, "no-matching-key"); - return false; - } - - if (key.match(suppliedKey)) { - return success(username, session, sd); - } - return false; - } catch (ExecutionException e) { - sd.authenticationError(username, "user-not-found"); - return false; - } - } - - boolean success(String username, ServerSession session, SshSession sd) { - sd.authenticationSuccess(username); - /* - * sshLog.onLogin(); - * - * GerritServerSession s = (GerritServerSession) session; - * s.addCloseSessionListener( new SshFutureListener() { - * - * @Override public void operationComplete(CloseFuture future) { final - * Context ctx = sshScope.newContext(null, sd, null); final Context old = - * sshScope.set(ctx); try { sshLog.onLogout(); } finally { - * sshScope.set(old); } } }); } - */ - return true; - } - - private static class SshKeyCacheWeigher implements - Weigher { - @Override - public int weigh(String key, SshKeyCacheEntry value) { - return key.length() + value.weigh(); - } - } + boolean validate(String username, SshSession sd) { + // now that the key has been validated, check with the authentication + // manager to ensure that this user exists and can authenticate + sd.authenticationSuccess(username); + UserModel user = gitblit.authenticate(sd); + if (user != null) { + return true; + } + sd.authenticationError(username, "user-not-found"); + return false; + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java b/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java deleted file mode 100644 index ddc48b35..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyCacheEntry.java +++ /dev/null @@ -1,26 +0,0 @@ - -package com.gitblit.transport.ssh; - -import java.security.PublicKey; - -class SshKeyCacheEntry { - private final String user; - private final PublicKey publicKey; - - SshKeyCacheEntry(String user, PublicKey publicKey) { - this.user = user; - this.publicKey = publicKey; - } - - String getUser() { - return user; - } - - boolean match(PublicKey inkey) { - return publicKey.equals(inkey); - } - - int weigh() { - return publicKey.getEncoded().length; - } -} -- cgit v1.2.3 From 58baee31462f6b3c60546d450e89e21b897412ce Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 20:12:36 -0500 Subject: Display ssh clone urls in the repository url panel --- src/main/java/com/gitblit/GitBlit.java | 10 ++++ .../java/com/gitblit/manager/ServicesManager.java | 63 +++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index bbc8bd37..a15bed89 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -121,6 +121,7 @@ public class GitBlit extends GitblitManager { String username = StringUtils.encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username); List list = new ArrayList(); + // http/https url if (settings.getBoolean(Keys.git.enableGitServlet, true)) { AccessPermission permission = user.getRepositoryPermission(repository).permission; @@ -129,6 +130,15 @@ public class GitBlit extends GitblitManager { } } + // ssh daemon url + String sshDaemonUrl = servicesManager.getSshDaemonUrl(request, user, repository); + if (!StringUtils.isEmpty(sshDaemonUrl)) { + AccessPermission permission = servicesManager.getSshDaemonAccessPermission(user, repository); + if (permission.exceeds(AccessPermission.NONE)) { + list.add(new RepositoryUrl(sshDaemonUrl, permission)); + } + } + // git daemon url String gitDaemonUrl = servicesManager.getGitDaemonUrl(request, user, repository); if (!StringUtils.isEmpty(gitDaemonUrl)) { diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index f75c6d14..1f494055 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -16,6 +16,7 @@ package com.gitblit.manager; import java.io.IOException; +import java.net.URI; import java.text.MessageFormat; import java.util.Arrays; import java.util.Date; @@ -199,8 +200,8 @@ public class ServicesManager implements IManager { return null; } if (user.canClone(repository)) { - String servername = request.getServerName(); - String url = gitDaemon.formatUrl(servername, repository.name); + String hostname = getHostname(request); + String url = gitDaemon.formatUrl(hostname, repository.name); return url; } } @@ -226,6 +227,64 @@ public class ServicesManager implements IManager { return AccessPermission.NONE; } + public String getSshDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) { + if (sshDaemon != null) { + String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); + if (bindInterface.equals("localhost") + && (!request.getServerName().equals("localhost") && !request.getServerName().equals("127.0.0.1"))) { + // ssh daemon is bound to localhost and the request is from elsewhere + return null; + } + if (user.canClone(repository)) { + String hostname = getHostname(request); + String url = sshDaemon.formatUrl(user.username, hostname, repository.name); + return url; + } + } + return null; + } + + public AccessPermission getSshDaemonAccessPermission(UserModel user, RepositoryModel repository) { + if (sshDaemon != null && user.canClone(repository)) { + AccessPermission sshDaemonPermission = user.getRepositoryPermission(repository).permission; + if (sshDaemonPermission.atLeast(AccessPermission.CLONE)) { + if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) { + // can not authenticate clone via anonymous ssh protocol + sshDaemonPermission = AccessPermission.NONE; + } else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) { + // can not authenticate push via anonymous ssh protocol + sshDaemonPermission = AccessPermission.CLONE; + } else { + // normal user permission + } + } + return sshDaemonPermission; + } + return AccessPermission.NONE; + } + + /** + * Extract the hostname from the canonical url or return the + * hostname from the servlet request. + * + * @param request + * @return + */ + protected String getHostname(HttpServletRequest request) { + String hostname = request.getServerName(); + String canonicalUrl = gitblit.getSettings().getString(Keys.web.canonicalUrl, null); + if (!StringUtils.isEmpty(canonicalUrl)) { + try { + URI uri = new URI(canonicalUrl); + String host = uri.getHost(); + if (!StringUtils.isEmpty(host) && !"localhost".equals(host)) { + hostname = host; + } + } catch (Exception e) { + } + } + return hostname; + } private class FederationPuller extends FederationPullService { -- cgit v1.2.3 From 311c135b9538e92b95d25cfb8c4d937d817677ca Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 20:34:26 -0500 Subject: Cleanup imports --- src/main/java/com/gitblit/transport/ssh/commands/Upload.java | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java index d6c3f961..44543f42 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java @@ -15,15 +15,10 @@ */ package com.gitblit.transport.ssh.commands; -import javax.inject.Inject; - import org.eclipse.jgit.transport.UploadPack; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.SshSession; @CommandMetaData(name = "git-upload-pack", description = "Upload pack") public class Upload extends AbstractGitCommand { -- cgit v1.2.3 From 8b63e0aaf044b36627e9ce02a1d73618e50700e4 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 20:48:28 -0500 Subject: Listen for ssh clients on all interfaces, by default --- src/main/distrib/data/gitblit.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index fbde84a7..bb16b6a5 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -108,7 +108,7 @@ git.sshPort = 29418 # # SINCE 1.5.0 # RESTART REQUIRED -git.sshBindInterface = localhost +git.sshBindInterface = # Directory for storing user SSH keys. # -- cgit v1.2.3 From b5361179d924eab162e17d7923f60d91cffb2d08 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sat, 8 Mar 2014 22:09:18 -0500 Subject: Extract key manager interface and implement a file-based key manager --- src/main/distrib/data/gitblit.properties | 11 +- .../com/gitblit/transport/ssh/FileKeyManager.java | 154 +++++++++++++++++++++ .../com/gitblit/transport/ssh/IKeyManager.java | 39 ++++++ .../com/gitblit/transport/ssh/NullKeyManager.java | 66 +++++++++ .../java/com/gitblit/transport/ssh/SshDaemon.java | 62 ++++++++- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 49 ++----- .../transport/ssh/SshPasswordAuthenticator.java | 10 +- 7 files changed, 342 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/FileKeyManager.java create mode 100644 src/main/java/com/gitblit/transport/ssh/IKeyManager.java create mode 100644 src/main/java/com/gitblit/transport/ssh/NullKeyManager.java (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index bb16b6a5..2338cc5b 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -110,7 +110,16 @@ git.sshPort = 29418 # RESTART REQUIRED git.sshBindInterface = -# Directory for storing user SSH keys. +# Specify the SSH key manager to use for retrieving, storing, and removing +# SSH keys. +# +# Valid key managers are: +# com.gitblit.transport.ssh.FileKeyManager +# +# SINCE 1.5.0 +git.sshKeysManager = com.gitblit.transport.ssh.FileKeyManager + +# Directory for storing user SSH keys when using the FileKeyManager. # # SINCE 1.5.0 git.sshKeysFolder= ${baseFolder}/ssh diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java new file mode 100644 index 00000000..87912cae --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -0,0 +1,154 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.File; +import java.io.IOException; +import java.security.PublicKey; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.codec.binary.Base64; +import org.apache.sshd.common.util.Buffer; +import org.eclipse.jgit.lib.Constants; + +import com.gitblit.Keys; +import com.gitblit.manager.IRuntimeManager; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +/** + * Manages SSH keys on the filesystem. + * + * @author James Moger + * + */ +public class FileKeyManager implements IKeyManager { + + protected final IRuntimeManager runtimeManager; + + public FileKeyManager(IRuntimeManager runtimeManager) { + this.runtimeManager = runtimeManager; + } + + @Override + public String toString() { + File dir = runtimeManager.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh"); + return MessageFormat.format("{0} ({1})", getClass().getSimpleName(), dir); + } + + @Override + public FileKeyManager start() { + return this; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public FileKeyManager stop() { + return this; + } + + @Override + public List getKeys(String username) { + try { + File keys = getKeystore(username); + if (!keys.exists()) { + return null; + } + if (keys.exists()) { + String str = Files.toString(keys, Charsets.ISO_8859_1); + String [] entries = str.split("\n"); + List list = new ArrayList(); + for (String entry : entries) { + if (entry.trim().length() == 0) { + // skip blanks + continue; + } + if (entry.charAt(0) == '#') { + // skip comments + continue; + } + final String[] parts = entry.split(" "); + final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1])); + list.add(new Buffer(bin).getRawPublicKey()); + } + + if (list.isEmpty()) { + return null; + } + return list; + } + } catch (IOException e) { + throw new RuntimeException("Canot read ssh keys", e); + } + return null; + } + + @Override + public boolean addKey(String username, String data) { + try { + File keys = getKeystore(username); + Files.append(data + '\n', keys, Charsets.ISO_8859_1); + return true; + } catch (IOException e) { + throw new RuntimeException("Cannot add ssh key", e); + } + } + + @Override + public boolean removeKey(String username, String data) { + try { + File keystore = getKeystore(username); + if (keystore.exists()) { + String str = Files.toString(keystore, Charsets.ISO_8859_1); + List keep = new ArrayList(); + String [] entries = str.split("\n"); + for (String entry : entries) { + if (entry.trim().length() == 0) { + // keep blanks + keep.add(entry); + continue; + } + if (entry.charAt(0) == '#') { + // keep comments + keep.add(entry); + continue; + } + final String[] parts = entry.split(" "); + if (!parts[1].equals(data)) { + keep.add(entry); + } + } + return true; + } + } catch (IOException e) { + throw new RuntimeException("Cannot remove ssh key", e); + } + return false; + } + + protected File getKeystore(String username) { + File dir = runtimeManager.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh"); + dir.mkdirs(); + File keys = new File(dir, username + ".keys"); + return keys; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java new file mode 100644 index 00000000..8b94fd60 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; +import java.util.List; + +/** + * + * @author James Moger + * + */ +public interface IKeyManager { + + IKeyManager start(); + + boolean isReady(); + + IKeyManager stop(); + + List getKeys(String username); + + boolean addKey(String username, String data); + + boolean removeKey(String username, String data); +} diff --git a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java new file mode 100644 index 00000000..2a2ef364 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; +import java.util.List; + +/** + * Rejects all SSH key management requests. + * + * @author James Moger + * + */ +public class NullKeyManager implements IKeyManager { + + public NullKeyManager() { + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + @Override + public NullKeyManager start() { + return this; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public NullKeyManager stop() { + return this; + } + + @Override + public List getKeys(String username) { + return null; + } + + @Override + public boolean addKey(String username, String data) { + return false; + } + + @Override + public boolean removeKey(String username, String data) { + return false; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index cc938bc1..de57f5ff 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -21,6 +21,8 @@ import java.net.InetSocketAddress; import java.text.MessageFormat; import java.util.concurrent.atomic.AtomicBoolean; +import javax.inject.Singleton; + import org.apache.sshd.SshServer; import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; import org.eclipse.jgit.internal.JGitText; @@ -42,6 +44,10 @@ import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; import com.gitblit.utils.WorkQueue; +import dagger.Module; +import dagger.ObjectGraph; +import dagger.Provides; + /** * Manager for the ssh transport. Roughly analogous to the * {@link com.gitblit.transport.git.GitDaemon} class. @@ -65,9 +71,9 @@ public class SshDaemon { private final AtomicBoolean run; - @SuppressWarnings("unused") private final IGitblit gitblit; private final SshServer sshd; + private final ObjectGraph injector; /** * Construct the Gitblit SSH daemon. @@ -76,12 +82,15 @@ public class SshDaemon { */ public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) { this.gitblit = gitblit; - + this.injector = ObjectGraph.create(new SshModule()); + IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); + IKeyManager keyManager = getKeyManager(); + InetSocketAddress addr; if (StringUtils.isEmpty(bindInterface)) { addr = new InetSocketAddress(port); @@ -94,7 +103,7 @@ public class SshDaemon { sshd.setHost(addr.getHostName()); sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); - sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(gitblit)); + sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(keyManager, gitblit)); sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit)); sshd.setSessionFactory(new SshSessionFactory(idGenerator)); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); @@ -176,4 +185,51 @@ public class SshDaemon { } } } + + protected IKeyManager getKeyManager() { + IKeyManager keyManager = null; + IStoredSettings settings = gitblit.getSettings(); + String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName()); + if (StringUtils.isEmpty(clazz)) { + clazz = FileKeyManager.class.getName(); + } + try { + Class managerClass = (Class) Class.forName(clazz); + keyManager = injector.get(managerClass).start(); + if (keyManager.isReady()) { + log.info("{} is ready.", keyManager); + } else { + log.warn("{} is disabled.", keyManager); + } + } catch (Exception e) { + log.error("failed to create ssh key manager " + clazz, e); + keyManager = injector.get(NullKeyManager.class).start(); + } + return keyManager; + } + + /** + * A nested Dagger graph is used for constructor dependency injection of + * complex classes. + * + * @author James Moger + * + */ + @Module( + library = true, + injects = { + NullKeyManager.class, + FileKeyManager.class + } + ) + class SshModule { + + @Provides @Singleton NullKeyManager provideNullKeyManager() { + return new NullKeyManager(); + } + + @Provides @Singleton FileKeyManager provideFileKeyManager() { + return new FileKeyManager(SshDaemon.this.gitblit); + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index 4cda268e..d41afdd0 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -15,29 +15,20 @@ */ package com.gitblit.transport.ssh; -import java.io.File; -import java.io.IOException; import java.security.PublicKey; -import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import org.apache.commons.codec.binary.Base64; -import org.apache.sshd.common.util.Buffer; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; -import org.eclipse.jgit.lib.Constants; -import com.gitblit.Keys; -import com.gitblit.manager.IGitblit; +import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; -import com.google.common.base.Charsets; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.io.Files; /** * @@ -46,7 +37,9 @@ import com.google.common.io.Files; */ public class SshKeyAuthenticator implements PublickeyAuthenticator { - protected final IGitblit gitblit; + protected final IKeyManager keyManager; + + protected final IAuthenticationManager authManager; LoadingCache> sshKeyCache = CacheBuilder .newBuilder(). @@ -54,37 +47,13 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { maximumSize(100) .build(new CacheLoader>() { public List load(String username) { - try { - File dir = gitblit.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh"); - dir.mkdirs(); - File keys = new File(dir, username + ".keys"); - if (!keys.exists()) { - return null; - } - if (keys.exists()) { - String str = Files.toString(keys, Charsets.ISO_8859_1); - String [] entries = str.split("\n"); - List list = new ArrayList(); - for (String entry : entries) { - final String[] parts = entry.split(" "); - final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1])); - list.add(new Buffer(bin).getRawPublicKey()); - } - - if (list.isEmpty()) { - return null; - } - return list; - } - } catch (IOException e) { - throw new RuntimeException("Canot read public key", e); - } - return null; + return keyManager.getKeys(username); } }); - public SshKeyAuthenticator(IGitblit gitblit) { - this.gitblit = gitblit; + public SshKeyAuthenticator(IKeyManager keyManager, IAuthenticationManager authManager) { + this.keyManager = keyManager; + this.authManager = authManager; } @Override @@ -115,7 +84,7 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { // now that the key has been validated, check with the authentication // manager to ensure that this user exists and can authenticate sd.authenticationSuccess(username); - UserModel user = gitblit.authenticate(sd); + UserModel user = authManager.authenticate(sd); if (user != null) { return true; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java index e39b5f72..ce01df76 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java @@ -20,7 +20,7 @@ import java.util.Locale; import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.session.ServerSession; -import com.gitblit.manager.IGitblit; +import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; /** @@ -30,16 +30,16 @@ import com.gitblit.models.UserModel; */ public class SshPasswordAuthenticator implements PasswordAuthenticator { - protected final IGitblit gitblit; + protected final IAuthenticationManager authManager; - public SshPasswordAuthenticator(IGitblit gitblit) { - this.gitblit = gitblit; + public SshPasswordAuthenticator(IAuthenticationManager authManager) { + this.authManager = authManager; } @Override public boolean authenticate(String username, String password, ServerSession session) { username = username.toLowerCase(Locale.US); - UserModel user = gitblit.authenticate(username, password.toCharArray()); + UserModel user = authManager.authenticate(username, password.toCharArray()); if (user != null) { SshSession sd = session.getAttribute(SshSession.KEY); sd.authenticationSuccess(username); -- cgit v1.2.3 From 9ba6bc127276f4b092849336f793aac2135f7887 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Thu, 13 Mar 2014 23:39:26 +0100 Subject: Bump SSHD version to 0.10.1 Change-Id: Ic7e204fb7ee2eb172f4c8424fc3d4b7e6c631ddd --- .classpath | 4 +-- build.moxie | 4 ++- gitblit.iml | 12 ++++---- .../transport/ssh/DisabledFilesystemFactory.java | 6 ++-- .../transport/ssh/GitblitServerSession.java | 35 ++++++++++++++++++++++ .../gitblit/transport/ssh/NonForwardingFilter.java | 15 +++++----- .../java/com/gitblit/transport/ssh/SshDaemon.java | 2 +- .../gitblit/transport/ssh/SshSessionFactory.java | 35 +++++++++++++++------- 8 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java (limited to 'src') diff --git a/.classpath b/.classpath index 52377e2a..d3aec7e8 100644 --- a/.classpath +++ b/.classpath @@ -52,8 +52,8 @@ - - + + diff --git a/build.moxie b/build.moxie index 7c392266..6015becc 100644 --- a/build.moxie +++ b/build.moxie @@ -109,7 +109,8 @@ properties: { bouncycastle.version : 1.49 selenium.version : 2.28.0 wikitext.version : 1.4 - sshd.version: 0.6.0 + sshd.version: 0.10.1 + mina.version: 2.0.7 } # Dependencies @@ -157,6 +158,7 @@ dependencies: - compile 'org.bouncycastle:bcmail-jdk15on:${bouncycastle.version}' :war :authority - compile 'org.bouncycastle:bcpkix-jdk15on:${bouncycastle.version}' :war :authority - compile 'org.apache.sshd:sshd-core:${sshd.version}' :war !org.easymock +- compile 'org.apache.mina:mina-core:${mina.version}' :war !org.easymock - compile 'rome:rome:0.9' :war :manager :api - compile 'com.google.code.gson:gson:1.7.2' :war :fedclient :manager :api - compile 'org.codehaus.groovy:groovy-all:${groovy.version}' :war diff --git a/gitblit.iml b/gitblit.iml index c114d4d0..a48f12ad 100644 --- a/gitblit.iml +++ b/gitblit.iml @@ -529,24 +529,24 @@ - + - + - + - + - + - + diff --git a/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java index 32a896b8..c0578f9d 100644 --- a/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java @@ -3,9 +3,9 @@ package com.gitblit.transport.ssh; import java.io.IOException; import org.apache.sshd.common.Session; -import org.apache.sshd.server.FileSystemFactory; -import org.apache.sshd.server.FileSystemView; -import org.apache.sshd.server.SshFile; +import org.apache.sshd.common.file.FileSystemFactory; +import org.apache.sshd.common.file.FileSystemView; +import org.apache.sshd.common.file.SshFile; public class DisabledFilesystemFactory implements FileSystemFactory { diff --git a/src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java b/src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java new file mode 100644 index 00000000..e5336025 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.server.ServerFactoryManager; +import org.apache.sshd.server.session.ServerSession; + +// Expose addition of close session listeners +class GitblitServerSession extends ServerSession { + + GitblitServerSession(ServerFactoryManager server, IoSession ioSession) + throws Exception { + super(server, ioSession); + } + + void addCloseSessionListener(SshFutureListener l) { + closeFuture.addListener(l); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java index 82f84267..0ed7926c 100644 --- a/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java +++ b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java @@ -1,28 +1,27 @@ package com.gitblit.transport.ssh; -import java.net.InetSocketAddress; - -import org.apache.sshd.server.ForwardingFilter; -import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.common.ForwardingFilter; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.SshdSocketAddress; public class NonForwardingFilter implements ForwardingFilter { @Override - public boolean canForwardAgent(ServerSession session) { + public boolean canConnect(SshdSocketAddress address, Session session) { return false; } @Override - public boolean canForwardX11(ServerSession session) { + public boolean canForwardAgent(Session session) { return false; } @Override - public boolean canConnect(InetSocketAddress address, ServerSession session) { + public boolean canForwardX11(Session session) { return false; } @Override - public boolean canListen(InetSocketAddress address, ServerSession session) { + public boolean canListen(SshdSocketAddress address, Session session) { return false; } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index de57f5ff..5bd397d7 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -107,7 +107,7 @@ public class SshDaemon { sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit)); sshd.setSessionFactory(new SshSessionFactory(idGenerator)); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); - sshd.setForwardingFilter(new NonForwardingFilter()); + sshd.setTcpipForwardingFilter(new NonForwardingFilter()); DispatchCommand gitblitCmd = new DispatchCommand(); gitblitCmd.registerCommand(CreateRepository.class); diff --git a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java index ef513404..4f61047d 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java @@ -17,11 +17,12 @@ package com.gitblit.transport.ssh; import java.net.SocketAddress; -import org.apache.mina.core.future.IoFuture; -import org.apache.mina.core.future.IoFutureListener; -import org.apache.mina.core.session.IoSession; import org.apache.mina.transport.socket.SocketSessionConfig; -import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.io.mina.MinaSession; +import org.apache.sshd.common.session.AbstractSession; import org.apache.sshd.server.session.SessionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,25 +46,37 @@ public class SshSessionFactory extends SessionFactory { } @Override - protected ServerSession createSession(final IoSession io) throws Exception { + protected AbstractSession createSession(final IoSession io) + throws Exception { log.info("connection accepted on " + io); - if (io.getConfig() instanceof SocketSessionConfig) { - final SocketSessionConfig c = (SocketSessionConfig) io.getConfig(); - c.setKeepAlive(true); + if (io instanceof MinaSession) { + if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) { + ((SocketSessionConfig) ((MinaSession) io).getSession() + .getConfig()).setKeepAlive(true); + } } - final ServerSession s = (ServerSession) super.createSession(io); + final GitblitServerSession s = (GitblitServerSession) super + .createSession(io); SocketAddress peer = io.getRemoteAddress(); SshSession session = new SshSession(idGenerator.next(), peer); s.setAttribute(SshSession.KEY, session); - io.getCloseFuture().addListener(new IoFutureListener() { + // TODO(davido): Log a session close without authentication as a + // failure. + s.addCloseSessionListener(new SshFutureListener() { @Override - public void operationComplete(IoFuture future) { + public void operationComplete(CloseFuture future) { log.info("connection closed on " + io); } }); return s; } + + @Override + protected AbstractSession doCreateSession(IoSession ioSession) + throws Exception { + return new GitblitServerSession(server, ioSession); + } } -- cgit v1.2.3 From bf4fc5c25ec31566b0fc1ee2e5e8bc15e5512893 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Fri, 14 Mar 2014 00:34:50 +0100 Subject: Add support for NIO2 IoSession Starting from version 0.9.0 Apache SSHD project added support for NIO2 IoSession. To use the new NIO2 session the `backend` option must be set to `NIO2`. By default, `NIO2`. Change-Id: I06cf92b02e80ecf9e8bfbd9f6d6d623dfe3ccff3 --- src/main/distrib/data/gitblit.properties | 5 +++++ src/main/java/com/gitblit/transport/ssh/SshDaemon.java | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 2338cc5b..64a52f5c 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -124,6 +124,11 @@ git.sshKeysManager = com.gitblit.transport.ssh.FileKeyManager # SINCE 1.5.0 git.sshKeysFolder= ${baseFolder}/ssh +# SSH backend NIO2|MINA. +# +# SINCE 1.5.0 +git.sshBackend = NIO2 + # Allow push/pull over http/https with JGit servlet. # If you do NOT want to allow Git clients to clone/push to Gitblit set this # to false. You might want to do this if you are only using ssh:// or git://. diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 5bd397d7..f0429a7c 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -24,6 +24,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Singleton; import org.apache.sshd.SshServer; +import org.apache.sshd.common.io.IoServiceFactory; +import org.apache.sshd.common.io.IoServiceFactoryFactory; +import org.apache.sshd.common.io.mina.MinaServiceFactory; +import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; +import org.apache.sshd.common.io.nio2.Nio2ServiceFactory; +import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; import org.eclipse.jgit.internal.JGitText; import org.slf4j.Logger; @@ -59,6 +65,10 @@ public class SshDaemon { private final Logger log = LoggerFactory.getLogger(SshDaemon.class); + public static enum SshSessionBackend { + MINA, NIO2 + } + /** * 22: IANA assigned port number for ssh. Note that this is a distinct * concept from gitblit's default conf for ssh port -- this "default" is @@ -90,6 +100,14 @@ public class SshDaemon { "localhost"); IKeyManager keyManager = getKeyManager(); + + String sshBackendStr = settings.getString(Keys.git.sshBackend, + SshSessionBackend.NIO2.name()); + SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr); + System.setProperty(IoServiceFactoryFactory.class.getName(), + backend == SshSessionBackend.MINA + ? MinaServiceFactoryFactory.class.getName() + : Nio2ServiceFactoryFactory.class.getName()); InetSocketAddress addr; if (StringUtils.isEmpty(bindInterface)) { -- cgit v1.2.3 From 5eafd9b6f046a03eca0576ae14673be674b9ce01 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Tue, 11 Mar 2014 21:44:38 +0100 Subject: Add set account SSH command Change-Id: I33d343dd34f93eaff9581d54f825f8c486e7c17f --- .../com/gitblit/transport/ssh/FileKeyManager.java | 8 +- .../com/gitblit/transport/ssh/IKeyManager.java | 2 + .../com/gitblit/transport/ssh/NullKeyManager.java | 5 + .../java/com/gitblit/transport/ssh/SshDaemon.java | 7 +- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 9 ++ .../transport/ssh/commands/DispatchCommand.java | 10 ++ .../transport/ssh/commands/SetAccountCommand.java | 118 +++++++++++++++++++++ 7 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index 87912cae..f590ab2f 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.lib.Constants; import com.gitblit.Keys; import com.gitblit.manager.IRuntimeManager; +import com.gitblit.utils.FileUtils; import com.google.common.base.Charsets; import com.google.common.io.Files; @@ -144,7 +145,12 @@ public class FileKeyManager implements IKeyManager { } return false; } - + + @Override + public boolean removeAllKey(String username) { + return FileUtils.delete(getKeystore(username)); + } + protected File getKeystore(String username) { File dir = runtimeManager.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh"); dir.mkdirs(); diff --git a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java index 8b94fd60..3528bf19 100644 --- a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java @@ -36,4 +36,6 @@ public interface IKeyManager { boolean addKey(String username, String data); boolean removeKey(String username, String data); + + boolean removeAllKey(String username); } diff --git a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java index 2a2ef364..6990d0de 100644 --- a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java @@ -63,4 +63,9 @@ public class NullKeyManager implements IKeyManager { public boolean removeKey(String username, String data) { return false; } + + @Override + public boolean removeAllKey(String username) { + return false; + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index f0429a7c..f2e1db7e 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -44,6 +44,7 @@ import com.gitblit.manager.IGitblit; import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; import com.gitblit.transport.ssh.commands.Receive; +import com.gitblit.transport.ssh.commands.SetAccountCommand; import com.gitblit.transport.ssh.commands.Upload; import com.gitblit.transport.ssh.commands.VersionCommand; import com.gitblit.utils.IdGenerator; @@ -116,12 +117,14 @@ public class SshDaemon { addr = new InetSocketAddress(bindInterface, port); } + SshKeyAuthenticator publickeyAuthenticator = new SshKeyAuthenticator( + keyManager, gitblit); sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); sshd.setHost(addr.getHostName()); sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); - sshd.setPublickeyAuthenticator(new SshKeyAuthenticator(keyManager, gitblit)); + sshd.setPublickeyAuthenticator(publickeyAuthenticator); sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit)); sshd.setSessionFactory(new SshSessionFactory(idGenerator)); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); @@ -130,6 +133,7 @@ public class SshDaemon { DispatchCommand gitblitCmd = new DispatchCommand(); gitblitCmd.registerCommand(CreateRepository.class); gitblitCmd.registerCommand(VersionCommand.class); + gitblitCmd.registerCommand(SetAccountCommand.class); DispatchCommand gitCmd = new DispatchCommand(); gitCmd.registerCommand(Upload.class); @@ -142,6 +146,7 @@ public class SshDaemon { root.setRepositoryResolver(new RepositoryResolver(gitblit)); root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); + root.setAuthenticator(publickeyAuthenticator); SshCommandFactory commandFactory = new SshCommandFactory( new WorkQueue(idGenerator), diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index d41afdd0..f1bff4f5 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -26,6 +26,7 @@ import org.apache.sshd.server.session.ServerSession; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; +import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -91,4 +92,12 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { sd.authenticationError(username, "user-not-found"); return false; } + + public IKeyManager getKeyManager() { + return keyManager; + } + + public Cache> getKeyCache() { + return sshKeyCache; + } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 597b9ea1..31b718e0 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -32,6 +32,7 @@ import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.SshKeyAuthenticator; import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; @@ -196,6 +197,10 @@ public class DispatchCommand extends BaseCommand { d.setRepositoryResolver(repositoryResolver); d.setUploadPackFactory(gitblitUploadPackFactory); d.setReceivePackFactory(gitblitReceivePackFactory); + d.setAuthenticator(authenticator); + } else if (cmd instanceof SetAccountCommand) { + SetAccountCommand setAccountCommand = (SetAccountCommand)cmd; + setAccountCommand.setAuthenticator(authenticator); } } @@ -213,4 +218,9 @@ public class DispatchCommand extends BaseCommand { public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { this.gitblitReceivePackFactory = gitblitReceivePackFactory; } + + private SshKeyAuthenticator authenticator; + public void setAuthenticator(SshKeyAuthenticator authenticator) { + this.authenticator = authenticator; + } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java new file mode 100644 index 00000000..98d9aba2 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java @@ -0,0 +1,118 @@ +//Copyright (C) 2012 The Android Open Source Project +// +//Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.IKeyManager; +import com.gitblit.transport.ssh.SshKeyAuthenticator; +import com.google.common.base.Charsets; + +/** Set a user's account settings. **/ +@CommandMetaData(name = "set-account", description = "Change an account's settings") +public class SetAccountCommand extends SshCommand { + + private static final String ALL = "ALL"; + + @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id") + private String user; + + @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account") + private List addSshKeys = new ArrayList(); + + @Option(name = "--delete-ssh-key", metaVar = "-|KEY", usage = "public keys to delete from the account") + private List deleteSshKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + validate(); + setAccount(); + } + + private void validate() throws UnloggedFailure { + if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) { + throw new UnloggedFailure(1, "Only one option may use the stdin"); + } + if (deleteSshKeys.contains(ALL)) { + deleteSshKeys = Collections.singletonList(ALL); + } + } + + private void setAccount() throws IOException, UnloggedFailure { + addSshKeys = readSshKey(addSshKeys); + if (!addSshKeys.isEmpty()) { + addSshKeys(addSshKeys); + } + + deleteSshKeys = readSshKey(deleteSshKeys); + if (!deleteSshKeys.isEmpty()) { + deleteSshKeys(deleteSshKeys); + } + authenticator.getKeyCache().invalidate(user); + } + + private void addSshKeys(List sshKeys) throws UnloggedFailure, + IOException { + IKeyManager keyManager = authenticator.getKeyManager(); + for (String sshKey : sshKeys) { + keyManager.addKey(user, sshKey); + } + } + + private void deleteSshKeys(List sshKeys) { + IKeyManager keyManager = authenticator.getKeyManager(); + if (sshKeys.contains(ALL)) { + keyManager.removeAllKey(user); + } else { + for (String sshKey : sshKeys) { + keyManager.removeKey(user, sshKey); + } + } + } + + private List readSshKey(List sshKeys) + throws UnsupportedEncodingException, IOException { + if (!sshKeys.isEmpty()) { + String sshKey; + int idx = sshKeys.indexOf("-"); + if (idx >= 0) { + sshKey = ""; + BufferedReader br = new BufferedReader(new InputStreamReader( + in, Charsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + sshKey += line + "\n"; + } + sshKeys.set(idx, sshKey); + } + } + return sshKeys; + } + + private SshKeyAuthenticator authenticator; + public void setAuthenticator(SshKeyAuthenticator authenticator) { + this.authenticator = authenticator; + } +} -- cgit v1.2.3 From b799d545f37f7123aaa1ee1d0ff3b61f1f3cc8c2 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Wed, 12 Mar 2014 00:35:57 +0100 Subject: Add review SSH command Change-Id: Ia03c15608814346522c548ff2439ee672ccb0d28 --- src/main/java/com/gitblit/models/TicketModel.java | 13 +++- .../java/com/gitblit/transport/ssh/SshDaemon.java | 2 + .../transport/ssh/commands/ReviewCommand.java | 87 ++++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java index aced6d78..f843e993 100644 --- a/src/main/java/com/gitblit/models/TicketModel.java +++ b/src/main/java/com/gitblit/models/TicketModel.java @@ -35,6 +35,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; @@ -1152,7 +1153,8 @@ public class TicketModel implements Serializable, Comparable { } public static enum Score { - approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed(-2); + approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed( + -2); final int value; @@ -1168,6 +1170,15 @@ public class TicketModel implements Serializable, Comparable { public String toString() { return name().toLowerCase().replace('_', ' '); } + + public static Score fromScore(int score) { + for (Score s : values()) { + if (s.getValue() == score) { + return s; + } + } + throw new NoSuchElementException(String.valueOf(score)); + } } public static enum Field { diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index f2e1db7e..4a2239b8 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -44,6 +44,7 @@ import com.gitblit.manager.IGitblit; import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; import com.gitblit.transport.ssh.commands.Receive; +import com.gitblit.transport.ssh.commands.ReviewCommand; import com.gitblit.transport.ssh.commands.SetAccountCommand; import com.gitblit.transport.ssh.commands.Upload; import com.gitblit.transport.ssh.commands.VersionCommand; @@ -134,6 +135,7 @@ public class SshDaemon { gitblitCmd.registerCommand(CreateRepository.class); gitblitCmd.registerCommand(VersionCommand.class); gitblitCmd.registerCommand(SetAccountCommand.class); + gitblitCmd.registerCommand(ReviewCommand.class); DispatchCommand gitCmd = new DispatchCommand(); gitCmd.registerCommand(Upload.class); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java new file mode 100644 index 00000000..9ce73315 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java @@ -0,0 +1,87 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.util.HashSet; +import java.util.Set; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import com.gitblit.models.TicketModel.Change; +import com.gitblit.models.TicketModel.Patchset; +import com.gitblit.models.TicketModel.Score; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.wicket.GitBlitWebSession; + +@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets") +public class ReviewCommand extends SshCommand { + + private final static short REV_ID_LEN = 40; + private final Set patchSets = new HashSet(); + + @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review") + void addPatchSetId(final String token) { + try { + patchSets.add(parsePatchSet(token)); + } catch (UnloggedFailure e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + @Option(name = "--project", required = true, aliases = "-p", usage = "project containing the specified patch set(s)") + private String project; + + @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change(s)", metaVar = "MESSAGE") + private String changeComment; + + @Option(name = "--vote", aliases = "-v", usage = "vote on this patch set", metaVar = "VOTE") + private int vote; + + @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)") + private boolean submitChange; + + @Override + public void run() throws UnloggedFailure { + UserModel user = GitBlitWebSession.get().getUser(); + for (Patchset ps : patchSets) { + // review + Change change = new Change(user.username); + change.review(ps, Score.fromScore(vote), false); + // TODO(davido): add patchset comment + if (submitChange) { + // TODO(davido): merge (when desired and the change is mergeable) + } + } + } + + private Patchset parsePatchSet(String ps) throws UnloggedFailure { + // By commit? + // + if (ps.matches("^([0-9a-fA-F]{4," + REV_ID_LEN + "})$")) { + // TODO; parse + } + + // By older style change,patchset? + // + if (ps.matches("^[1-9][0-9]*,[1-9][0-9]*$")) { + // TODO: parse + } + + throw new UnloggedFailure(1, "fatal: Cannot parse patchset: " + ps); + } +} -- cgit v1.2.3 From 2b5484449ac92f570554936e71fca3f34be63eff Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:53:56 -0400 Subject: Display the selected SSH backend in the daemon startup message --- src/main/java/com/gitblit/transport/ssh/SshDaemon.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 4a2239b8..f6c4e0eb 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -24,11 +24,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import javax.inject.Singleton; import org.apache.sshd.SshServer; -import org.apache.sshd.common.io.IoServiceFactory; import org.apache.sshd.common.io.IoServiceFactoryFactory; -import org.apache.sshd.common.io.mina.MinaServiceFactory; import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; -import org.apache.sshd.common.io.nio2.Nio2ServiceFactory; import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; import org.apache.sshd.server.keyprovider.PEMGeneratorHostKeyProvider; import org.eclipse.jgit.internal.JGitText; @@ -187,9 +184,12 @@ public class SshDaemon { sshd.start(); run.set(true); + String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend, + SshSessionBackend.NIO2.name()); + log.info(MessageFormat.format( - "SSH Daemon is listening on {0}:{1,number,0}", - sshd.getHost(), sshd.getPort())); + "SSH Daemon ({0}) is listening on {1}:{2,number,0}", + sshBackendStr, sshd.getHost(), sshd.getPort())); } /** @return true if this daemon is receiving connections. */ -- cgit v1.2.3 From 0984f61898376c236abb5c678736ac0d8bded14d Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:54:34 -0400 Subject: Remove unused idGenerator from the SshSessionFactory --- src/main/java/com/gitblit/transport/ssh/SshDaemon.java | 2 +- src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index f6c4e0eb..152b8263 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -124,7 +124,7 @@ public class SshDaemon { gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); sshd.setPublickeyAuthenticator(publickeyAuthenticator); sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit)); - sshd.setSessionFactory(new SshSessionFactory(idGenerator)); + sshd.setSessionFactory(new SshSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); diff --git a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java index 4f61047d..ae6da3fb 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java @@ -27,8 +27,6 @@ import org.apache.sshd.server.session.SessionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.utils.IdGenerator; - /** * @@ -39,10 +37,7 @@ public class SshSessionFactory extends SessionFactory { private final Logger log = LoggerFactory.getLogger(getClass()); - private final IdGenerator idGenerator; - - public SshSessionFactory(IdGenerator idGenerator) { - this.idGenerator = idGenerator; + public SshSessionFactory() { } @Override -- cgit v1.2.3 From d5603a7722adc4bb9ddeeac31491ba93d8bb9128 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:11:20 -0400 Subject: Fix incorrect displayed SSH transport permission --- src/main/java/com/gitblit/GitBlit.java | 2 +- .../java/com/gitblit/manager/ServicesManager.java | 22 ++-------------------- 2 files changed, 3 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index a15bed89..817d18cb 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -133,7 +133,7 @@ public class GitBlit extends GitblitManager { // ssh daemon url String sshDaemonUrl = servicesManager.getSshDaemonUrl(request, user, repository); if (!StringUtils.isEmpty(sshDaemonUrl)) { - AccessPermission permission = servicesManager.getSshDaemonAccessPermission(user, repository); + AccessPermission permission = user.getRepositoryPermission(repository).permission; if (permission.exceeds(AccessPermission.NONE)) { list.add(new RepositoryUrl(sshDaemonUrl, permission)); } diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java index 1f494055..17724f2b 100644 --- a/src/main/java/com/gitblit/manager/ServicesManager.java +++ b/src/main/java/com/gitblit/manager/ServicesManager.java @@ -244,29 +244,11 @@ public class ServicesManager implements IManager { return null; } - public AccessPermission getSshDaemonAccessPermission(UserModel user, RepositoryModel repository) { - if (sshDaemon != null && user.canClone(repository)) { - AccessPermission sshDaemonPermission = user.getRepositoryPermission(repository).permission; - if (sshDaemonPermission.atLeast(AccessPermission.CLONE)) { - if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) { - // can not authenticate clone via anonymous ssh protocol - sshDaemonPermission = AccessPermission.NONE; - } else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) { - // can not authenticate push via anonymous ssh protocol - sshDaemonPermission = AccessPermission.CLONE; - } else { - // normal user permission - } - } - return sshDaemonPermission; - } - return AccessPermission.NONE; - } - + /** * Extract the hostname from the canonical url or return the * hostname from the servlet request. - * + * * @param request * @return */ -- cgit v1.2.3 From 8ee7c8d2f1f53c72c4f47dde1093cb3db1f27dcb Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:12:26 -0400 Subject: Fix deleteAllKeys method signature --- src/main/java/com/gitblit/transport/ssh/FileKeyManager.java | 2 +- src/main/java/com/gitblit/transport/ssh/IKeyManager.java | 2 +- src/main/java/com/gitblit/transport/ssh/NullKeyManager.java | 2 +- src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index f590ab2f..04b42fc8 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -147,7 +147,7 @@ public class FileKeyManager implements IKeyManager { } @Override - public boolean removeAllKey(String username) { + public boolean removeAllKeys(String username) { return FileUtils.delete(getKeystore(username)); } diff --git a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java index 3528bf19..cb32a020 100644 --- a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java @@ -37,5 +37,5 @@ public interface IKeyManager { boolean removeKey(String username, String data); - boolean removeAllKey(String username); + boolean removeAllKeys(String username); } diff --git a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java index 6990d0de..454d3cfc 100644 --- a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java @@ -65,7 +65,7 @@ public class NullKeyManager implements IKeyManager { } @Override - public boolean removeAllKey(String username) { + public boolean removeAllKeys(String username) { return false; } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java index 98d9aba2..767f3cb4 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java @@ -84,7 +84,7 @@ public class SetAccountCommand extends SshCommand { private void deleteSshKeys(List sshKeys) { IKeyManager keyManager = authenticator.getKeyManager(); if (sshKeys.contains(ALL)) { - keyManager.removeAllKey(user); + keyManager.removeAllKeys(user); } else { for (String sshKey : sshKeys) { keyManager.removeKey(user, sshKey); -- cgit v1.2.3 From 8384e02b7d6f1a1ecd6782c6b5f682c109cd6e04 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:15:04 -0400 Subject: Disregard comment/description for add/remove in FileKeyManager --- .../com/gitblit/transport/ssh/FileKeyManager.java | 104 +++++++++++++++------ 1 file changed, 78 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index 04b42fc8..1eb470bf 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -28,40 +28,40 @@ import org.eclipse.jgit.lib.Constants; import com.gitblit.Keys; import com.gitblit.manager.IRuntimeManager; -import com.gitblit.utils.FileUtils; import com.google.common.base.Charsets; +import com.google.common.base.Joiner; import com.google.common.io.Files; /** * Manages SSH keys on the filesystem. - * + * * @author James Moger * */ public class FileKeyManager implements IKeyManager { protected final IRuntimeManager runtimeManager; - + public FileKeyManager(IRuntimeManager runtimeManager) { this.runtimeManager = runtimeManager; } - + @Override public String toString() { File dir = runtimeManager.getFileOrFolder(Keys.git.sshKeysFolder, "${baseFolder}/ssh"); return MessageFormat.format("{0} ({1})", getClass().getSimpleName(), dir); } - + @Override public FileKeyManager start() { return this; } - + @Override public boolean isReady() { return true; } - + @Override public FileKeyManager stop() { return this; @@ -75,10 +75,8 @@ public class FileKeyManager implements IKeyManager { return null; } if (keys.exists()) { - String str = Files.toString(keys, Charsets.ISO_8859_1); - String [] entries = str.split("\n"); List list = new ArrayList(); - for (String entry : entries) { + for (String entry : Files.readLines(keys, Charsets.ISO_8859_1)) { if (entry.trim().length() == 0) { // skip blanks continue; @@ -91,7 +89,7 @@ public class FileKeyManager implements IKeyManager { final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1])); list.add(new Buffer(bin).getRawPublicKey()); } - + if (list.isEmpty()) { return null; } @@ -103,41 +101,88 @@ public class FileKeyManager implements IKeyManager { return null; } + /** + * Adds a unique key to the keystore. This function determines uniqueness + * by disregarding the comment/description field during key comparisons. + */ @Override public boolean addKey(String username, String data) { try { - File keys = getKeystore(username); - Files.append(data + '\n', keys, Charsets.ISO_8859_1); + String newKey = stripCommentFromKey(data); + + List lines = new ArrayList(); + File keystore = getKeystore(username); + if (keystore.exists()) { + for (String entry : Files.readLines(keystore, Charsets.ISO_8859_1)) { + String line = entry.trim(); + if (line.length() == 0) { + // keep blanks + lines.add(entry); + continue; + } + if (line.charAt(0) == '#') { + // keep comments + lines.add(entry); + continue; + } + + // only add keys that do not match the new key + String oldKey = stripCommentFromKey(line); + if (!newKey.equals(oldKey)) { + lines.add(entry); + } + } + } + + // add new key + lines.add(data); + + // write keystore + String content = Joiner.on("\n").join(lines).trim().concat("\n"); + Files.write(content, keystore, Charsets.ISO_8859_1); return true; } catch (IOException e) { throw new RuntimeException("Cannot add ssh key", e); } } - + + /** + * Removes a key from the keystore. + */ @Override public boolean removeKey(String username, String data) { try { + String rmKey = stripCommentFromKey(data); + File keystore = getKeystore(username); if (keystore.exists()) { - String str = Files.toString(keystore, Charsets.ISO_8859_1); - List keep = new ArrayList(); - String [] entries = str.split("\n"); - for (String entry : entries) { - if (entry.trim().length() == 0) { + List lines = new ArrayList(); + for (String entry : Files.readLines(keystore, Charsets.ISO_8859_1)) { + String line = entry.trim(); + if (line.length() == 0) { // keep blanks - keep.add(entry); + lines.add(entry); continue; } - if (entry.charAt(0) == '#') { + if (line.charAt(0) == '#') { // keep comments - keep.add(entry); + lines.add(entry); continue; } - final String[] parts = entry.split(" "); - if (!parts[1].equals(data)) { - keep.add(entry); + + // only include keys that are NOT rmKey + String oldKey = stripCommentFromKey(line); + if (!rmKey.equals(oldKey)) { + lines.add(entry); } } + if (lines.isEmpty()) { + keystore.delete(); + } else { + // write keystore + String content = Joiner.on("\n").join(lines).trim().concat("\n"); + Files.write(content, keystore, Charsets.ISO_8859_1); + } return true; } } catch (IOException e) { @@ -148,7 +193,7 @@ public class FileKeyManager implements IKeyManager { @Override public boolean removeAllKeys(String username) { - return FileUtils.delete(getKeystore(username)); + return getKeystore(username).delete(); } protected File getKeystore(String username) { @@ -157,4 +202,11 @@ public class FileKeyManager implements IKeyManager { File keys = new File(dir, username + ".keys"); return keys; } + + /* Strips the comment from the key data and eliminates whitespace diffs */ + protected String stripCommentFromKey(String data) { + String [] cols = data.split(" "); + String key = Joiner.on(" ").join(cols[0], cols[1]); + return key; + } } -- cgit v1.2.3 From 39ffede55f79395d6d1298f24ce201dbae262a32 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:15:57 -0400 Subject: Simplify version command and output --- .../com/gitblit/transport/ssh/commands/VersionCommand.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java index fc3e01b3..c2c4f52d 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java @@ -16,20 +16,14 @@ package com.gitblit.transport.ssh.commands; -import org.kohsuke.args4j.Option; - import com.gitblit.Constants; import com.gitblit.transport.ssh.CommandMetaData; -@CommandMetaData(name="version", description = "Print Gitblit version") +@CommandMetaData(name="version", description = "Display the Gitblit version") public class VersionCommand extends SshCommand { - @Option(name = "--verbose", aliases = {"-v"}, metaVar = "VERBOSE", usage = "Print verbose versions") - private boolean verbose; - @Override public void run() { - stdout.println(String.format("Version: %s", Constants.getGitBlitVersion(), - verbose)); + stdout.println(Constants.getGitBlitVersion()); } } -- cgit v1.2.3 From 44e2ee1d05a9d455ae60dd64058b31f006d551b7 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:10:25 -0400 Subject: Revise SSH public key integration with AuthenticationManager --- src/main/java/com/gitblit/Constants.java | 2 +- .../com/gitblit/git/GitblitUploadPackFactory.java | 18 +--------- .../java/com/gitblit/git/RepositoryResolver.java | 6 ++-- .../com/gitblit/manager/AuthenticationManager.java | 25 +++++++------- .../java/com/gitblit/manager/GitblitManager.java | 10 +++--- .../gitblit/manager/IAuthenticationManager.java | 12 +++++-- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 38 ++++++++++++---------- .../transport/ssh/SshPasswordAuthenticator.java | 15 +++++++-- 8 files changed, 67 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java index 889e5a30..56dfec06 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java @@ -501,7 +501,7 @@ public class Constants { } public static enum AuthenticationType { - SSH, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER; + PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER; public boolean isStandard() { return ordinal() <= COOKIE.ordinal(); diff --git a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java index a72d4ad9..7a476775 100644 --- a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java @@ -26,7 +26,6 @@ import org.eclipse.jgit.transport.resolver.UploadPackFactory; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; import com.gitblit.transport.git.GitDaemonClient; -import com.gitblit.transport.ssh.SshSession; /** * The upload pack factory creates an upload pack which controls what refs are @@ -48,28 +47,13 @@ public class GitblitUploadPackFactory implements UploadPackFactory { public UploadPack create(X req, Repository db) throws ServiceNotEnabledException, ServiceNotAuthorizedException { - UserModel user = UserModel.ANONYMOUS; int timeout = 0; - if (req instanceof HttpServletRequest) { - // http/https request may or may not be authenticated - HttpServletRequest client = (HttpServletRequest) req; - user = authenticationManager.authenticate(client); - if (user == null) { - user = UserModel.ANONYMOUS; - } - } else if (req instanceof GitDaemonClient) { + if (req instanceof GitDaemonClient) { // git daemon request is always anonymous GitDaemonClient client = (GitDaemonClient) req; // set timeout from Git daemon timeout = client.getDaemon().getTimeout(); - } else if (req instanceof SshSession) { - // SSH request is always authenticated - SshSession client = (SshSession) req; - user = authenticationManager.authenticate(client); - if (user == null) { - throw new ServiceNotAuthorizedException(); - } } UploadPack up = new UploadPack(db); diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java index 08048195..ad5dcf01 100644 --- a/src/main/java/com/gitblit/git/RepositoryResolver.java +++ b/src/main/java/com/gitblit/git/RepositoryResolver.java @@ -104,11 +104,9 @@ public class RepositoryResolver extends FileResolver { user = UserModel.ANONYMOUS; } } else if (req instanceof SshSession) { + // ssh is always authenticated SshSession s = (SshSession) req; - user = gitblit.authenticate(s); - if (user == null) { - throw new IOException(String.format("User %s not found", s.getRemoteUser())); - } + user = gitblit.getUserModel(s.getRemoteUser()); } if (user.canClone(model)) { diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java index 658c2890..10f8fd11 100644 --- a/src/main/java/com/gitblit/manager/AuthenticationManager.java +++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java @@ -17,6 +17,7 @@ package com.gitblit.manager; import java.nio.charset.Charset; import java.security.Principal; +import java.security.PublicKey; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; @@ -47,7 +48,6 @@ import com.gitblit.auth.SalesforceAuthProvider; import com.gitblit.auth.WindowsAuthProvider; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.Base64; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.StringUtils; @@ -291,28 +291,31 @@ public class AuthenticationManager implements IAuthenticationManager { } /** - * Authenticate a user based on SSH session. + * Authenticate a user based on a public key. * - * @param SshSession + * This implementation assumes that the authentication has already take place + * (e.g. SSHDaemon) and that this is a validation/verification of the user. + * + * @param username + * @param key * @return a user object or null */ @Override - public UserModel authenticate(SshSession sshSession) { - String username = sshSession.getRemoteUser(); + public UserModel authenticate(String username, PublicKey key) { if (username != null) { if (!StringUtils.isEmpty(username)) { UserModel user = userManager.getUserModel(username); if (user != null) { // existing user - logger.debug(MessageFormat.format("{0} authenticated by SSH key from {1}", - user.username, sshSession.getRemoteAddress())); - return validateAuthentication(user, AuthenticationType.SSH); + logger.debug(MessageFormat.format("{0} authenticated by {1} public key", + user.username, key.getAlgorithm())); + return validateAuthentication(user, AuthenticationType.PUBLIC_KEY); } - logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted ssh authentication from {1}", - username, sshSession.getRemoteAddress())); + logger.warn(MessageFormat.format("Failed to find UserModel for {0} during public key authentication", + username)); } } else { - logger.warn("Empty user in SSH session"); + logger.warn("Empty user passed to AuthenticationManager.authenticate!"); } return null; } diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index a5a26379..97e8efc9 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Type; +import java.security.PublicKey; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; @@ -68,7 +69,6 @@ import com.gitblit.models.SettingModel; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.tickets.ITicketService; -import com.gitblit.transport.ssh.SshSession; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JsonUtils; @@ -652,12 +652,12 @@ public class GitblitManager implements IGitblit { } return user; } - + @Override - public UserModel authenticate(SshSession sshSession) { - return authenticationManager.authenticate(sshSession); + public UserModel authenticate(String username, PublicKey key) { + return authenticationManager.authenticate(username, key); } - + @Override public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) { UserModel user = authenticationManager.authenticate(httpRequest, requiresCertificate); diff --git a/src/main/java/com/gitblit/manager/IAuthenticationManager.java b/src/main/java/com/gitblit/manager/IAuthenticationManager.java index 5d98d127..4f43d928 100644 --- a/src/main/java/com/gitblit/manager/IAuthenticationManager.java +++ b/src/main/java/com/gitblit/manager/IAuthenticationManager.java @@ -15,12 +15,13 @@ */ package com.gitblit.manager; +import java.security.PublicKey; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.SshSession; public interface IAuthenticationManager extends IManager { @@ -34,7 +35,14 @@ public interface IAuthenticationManager extends IManager { */ UserModel authenticate(HttpServletRequest httpRequest); - public UserModel authenticate(SshSession sshSession); + /** + * Authenticate a user based on a public key. + * + * @param username + * @param key + * @return a user object or null + */ + UserModel authenticate(String username, PublicKey key); /** * Authenticate a user based on HTTP request parameters. diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index f1bff4f5..044d2643 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -23,6 +23,8 @@ import java.util.concurrent.TimeUnit; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; @@ -38,6 +40,8 @@ import com.google.common.cache.LoadingCache; */ public class SshKeyAuthenticator implements PublickeyAuthenticator { + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final IKeyManager keyManager; protected final IAuthenticationManager authManager; @@ -47,6 +51,7 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { expireAfterAccess(15, TimeUnit.MINUTES). maximumSize(100) .build(new CacheLoader>() { + @Override public List load(String username) { return keyManager.getKeys(username); } @@ -60,43 +65,42 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { @Override public boolean authenticate(String username, final PublicKey suppliedKey, final ServerSession session) { - final SshSession sd = session.getAttribute(SshSession.KEY); + final SshSession client = session.getAttribute(SshSession.KEY); + + if (client.getRemoteUser() != null) { + // TODO why do we re-authenticate? + log.info("{} has already authenticated!", username); + return true; + } username = username.toLowerCase(Locale.US); try { List keys = sshKeyCache.get(username); if (keys == null || keys.isEmpty()) { - sd.authenticationError(username, "no-matching-key"); + log.info("{} has not added any public keys for ssh authentication", username); return false; } + for (PublicKey key : keys) { if (key.equals(suppliedKey)) { - return validate(username, sd); + UserModel user = authManager.authenticate(username, key); + if (user != null) { + client.authenticationSuccess(username); + return true; + } } } - return false; } catch (ExecutionException e) { - sd.authenticationError(username, "user-not-found"); - return false; } - } - boolean validate(String username, SshSession sd) { - // now that the key has been validated, check with the authentication - // manager to ensure that this user exists and can authenticate - sd.authenticationSuccess(username); - UserModel user = authManager.authenticate(sd); - if (user != null) { - return true; - } - sd.authenticationError(username, "user-not-found"); + log.warn("could not authenticate {} for SSH using the supplied public key", username); return false; } public IKeyManager getKeyManager() { return keyManager; } - + public Cache> getKeyCache() { return sshKeyCache; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java index ce01df76..3baf985d 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java @@ -19,6 +19,8 @@ import java.util.Locale; import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; @@ -30,6 +32,8 @@ import com.gitblit.models.UserModel; */ public class SshPasswordAuthenticator implements PasswordAuthenticator { + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final IAuthenticationManager authManager; public SshPasswordAuthenticator(IAuthenticationManager authManager) { @@ -38,13 +42,20 @@ public class SshPasswordAuthenticator implements PasswordAuthenticator { @Override public boolean authenticate(String username, String password, ServerSession session) { + SshSession client = session.getAttribute(SshSession.KEY); + if (client.getRemoteUser() != null) { + log.info("{} has already authenticated!", username); + return true; + } + username = username.toLowerCase(Locale.US); UserModel user = authManager.authenticate(username, password.toCharArray()); if (user != null) { - SshSession sd = session.getAttribute(SshSession.KEY); - sd.authenticationSuccess(username); + client.authenticationSuccess(username); return true; } + + log.warn("could not authenticate {} for SSH using the supplied password", username); return false; } } -- cgit v1.2.3 From a8dd379bc357c64d1128bc6790e681e27387dbee Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:43:55 -0400 Subject: Rename & simplify SshSession->SshDaemonClient --- .../com/gitblit/git/GitblitReceivePackFactory.java | 9 +- .../java/com/gitblit/git/RepositoryResolver.java | 22 ++-- .../gitblit/transport/ssh/AbstractGitCommand.java | 14 +-- .../gitblit/transport/ssh/SshCommandFactory.java | 2 +- .../java/com/gitblit/transport/ssh/SshContext.java | 10 +- .../java/com/gitblit/transport/ssh/SshDaemon.java | 6 +- .../com/gitblit/transport/ssh/SshDaemonClient.java | 64 ++++++++++++ .../gitblit/transport/ssh/SshKeyAuthenticator.java | 8 +- .../transport/ssh/SshPasswordAuthenticator.java | 6 +- .../java/com/gitblit/transport/ssh/SshSession.java | 111 --------------------- .../gitblit/transport/ssh/SshSessionFactory.java | 10 +- .../transport/ssh/commands/DispatchCommand.java | 14 +-- .../gitblit/transport/ssh/commands/Receive.java | 2 +- .../com/gitblit/transport/ssh/commands/Upload.java | 2 +- 14 files changed, 116 insertions(+), 164 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshSession.java (limited to 'src') diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java index b928d851..41e348ba 100644 --- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java @@ -32,7 +32,7 @@ import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.transport.git.GitDaemonClient; -import com.gitblit.transport.ssh.SshSession; +import com.gitblit.transport.ssh.SshDaemonClient; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.StringUtils; @@ -90,13 +90,12 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { // set timeout from Git daemon timeout = client.getDaemon().getTimeout(); - } else if (req instanceof SshSession) { + } else if (req instanceof SshDaemonClient) { // SSH request is always authenticated - SshSession client = (SshSession) req; + SshDaemonClient client = (SshDaemonClient) req; repositoryName = client.getRepositoryName(); origin = client.getRemoteAddress().toString(); - String username = client.getRemoteUser(); - user = gitblit.getUserModel(username); + user = client.getUser(); } boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false); diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java index ad5dcf01..cc13144e 100644 --- a/src/main/java/com/gitblit/git/RepositoryResolver.java +++ b/src/main/java/com/gitblit/git/RepositoryResolver.java @@ -31,7 +31,7 @@ import com.gitblit.manager.IGitblit; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; import com.gitblit.transport.git.GitDaemonClient; -import com.gitblit.transport.ssh.SshSession; +import com.gitblit.transport.ssh.SshDaemonClient; /** * Resolves repositories and grants export access. @@ -69,9 +69,9 @@ public class RepositoryResolver extends FileResolver { // git request GitDaemonClient client = (GitDaemonClient) req; client.setRepositoryName(name); - } else if (req instanceof SshSession) { - SshSession s = (SshSession)req; - s.setRepositoryName(name); + } else if (req instanceof SshDaemonClient) { + SshDaemonClient client = (SshDaemonClient) req; + client.setRepositoryName(name); } return repo; } @@ -96,17 +96,17 @@ public class RepositoryResolver extends FileResolver { user = UserModel.ANONYMOUS; } else if (req instanceof HttpServletRequest) { // http/https request - HttpServletRequest httpRequest = (HttpServletRequest) req; - scheme = httpRequest.getScheme(); - origin = httpRequest.getRemoteAddr(); - user = gitblit.authenticate(httpRequest); + HttpServletRequest client = (HttpServletRequest) req; + scheme = client.getScheme(); + origin = client.getRemoteAddr(); + user = gitblit.authenticate(client); if (user == null) { user = UserModel.ANONYMOUS; } - } else if (req instanceof SshSession) { + } else if (req instanceof SshDaemonClient) { // ssh is always authenticated - SshSession s = (SshSession) req; - user = gitblit.getUserModel(s.getRemoteUser()); + SshDaemonClient client = (SshDaemonClient) req; + user = client.getUser(); } if (user.canClone(model)) { diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java index bba6a402..188cb005 100644 --- a/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java @@ -36,9 +36,9 @@ public abstract class AbstractGitCommand extends BaseCommand { @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name") protected String repository; - protected RepositoryResolver repositoryResolver; - protected ReceivePackFactory receivePackFactory; - protected UploadPackFactory uploadPackFactory; + protected RepositoryResolver repositoryResolver; + protected ReceivePackFactory receivePackFactory; + protected UploadPackFactory uploadPackFactory; protected Repository repo; @@ -84,7 +84,7 @@ public abstract class AbstractGitCommand extends BaseCommand { } repository = repository.substring(1); try { - return repositoryResolver.open(ctx.getSession(), repository); + return repositoryResolver.open(ctx.getClient(), repository); } catch (Exception e) { throw new Failure(1, "fatal: '" + repository + "': not a git archive", e); @@ -92,17 +92,17 @@ public abstract class AbstractGitCommand extends BaseCommand { } public void setRepositoryResolver( - RepositoryResolver repositoryResolver) { + RepositoryResolver repositoryResolver) { this.repositoryResolver = repositoryResolver; } public void setReceivePackFactory( - GitblitReceivePackFactory receivePackFactory) { + GitblitReceivePackFactory receivePackFactory) { this.receivePackFactory = receivePackFactory; } public void setUploadPackFactory( - GitblitUploadPackFactory uploadPackFactory) { + GitblitUploadPackFactory uploadPackFactory) { this.uploadPackFactory = uploadPackFactory; } } \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index 0c8492f7..a52e62b8 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -133,7 +133,7 @@ public class SshCommandFactory implements CommandFactory { private void onStart() throws IOException { synchronized (this) { - SshContext ctx = new SshContext(session.getAttribute(SshSession.KEY), cmdLine); + SshContext ctx = new SshContext(session.getAttribute(SshDaemonClient.KEY), cmdLine); try { cmd = dispatcher; cmd.setArguments(argv); diff --git a/src/main/java/com/gitblit/transport/ssh/SshContext.java b/src/main/java/com/gitblit/transport/ssh/SshContext.java index b137cb87..4c5786e4 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshContext.java +++ b/src/main/java/com/gitblit/transport/ssh/SshContext.java @@ -17,16 +17,16 @@ package com.gitblit.transport.ssh; public class SshContext { - private final SshSession session; + private final SshDaemonClient client; private final String commandLine; - public SshContext(SshSession session, String commandLine) { - this.session = session; + public SshContext(SshDaemonClient client, String commandLine) { + this.client = client; this.commandLine = commandLine; } - public SshSession getSession() { - return session; + public SshDaemonClient getClient() { + return client; } public String getCommandLine() { diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 152b8263..81d78784 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -142,9 +142,9 @@ public class SshDaemon { root.registerDispatcher("gitblit", gitblitCmd); root.registerDispatcher("git", gitCmd); - root.setRepositoryResolver(new RepositoryResolver(gitblit)); - root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); - root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); + root.setRepositoryResolver(new RepositoryResolver(gitblit)); + root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); + root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); root.setAuthenticator(publickeyAuthenticator); SshCommandFactory commandFactory = new SshCommandFactory( diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java new file mode 100644 index 00000000..4d8ea4b6 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.net.SocketAddress; + +import org.apache.sshd.common.Session.AttributeKey; + +import com.gitblit.models.UserModel; + +/** + * + * @author Eric Myrhe + * + */ +public class SshDaemonClient { + public static final AttributeKey KEY = new AttributeKey(); + + private final SocketAddress remoteAddress; + + private volatile UserModel user; + private volatile String repositoryName; + + SshDaemonClient(SocketAddress peer) { + this.remoteAddress = peer; + } + + public SocketAddress getRemoteAddress() { + return remoteAddress; + } + + public UserModel getUser() { + return user; + } + + public void setUser(UserModel user) { + this.user = user; + } + + public String getUsername() { + return user == null ? null : user.username; + } + + public void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + public String getRepositoryName() { + return repositoryName; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index 044d2643..36319226 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -43,7 +43,7 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { protected final Logger log = LoggerFactory.getLogger(getClass()); protected final IKeyManager keyManager; - + protected final IAuthenticationManager authManager; LoadingCache> sshKeyCache = CacheBuilder @@ -65,9 +65,9 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { @Override public boolean authenticate(String username, final PublicKey suppliedKey, final ServerSession session) { - final SshSession client = session.getAttribute(SshSession.KEY); + final SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); - if (client.getRemoteUser() != null) { + if (client.getUser() != null) { // TODO why do we re-authenticate? log.info("{} has already authenticated!", username); return true; @@ -85,7 +85,7 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { if (key.equals(suppliedKey)) { UserModel user = authManager.authenticate(username, key); if (user != null) { - client.authenticationSuccess(username); + client.setUser(user); return true; } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java index 3baf985d..5ddc4a39 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java @@ -42,8 +42,8 @@ public class SshPasswordAuthenticator implements PasswordAuthenticator { @Override public boolean authenticate(String username, String password, ServerSession session) { - SshSession client = session.getAttribute(SshSession.KEY); - if (client.getRemoteUser() != null) { + SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); + if (client.getUser() != null) { log.info("{} has already authenticated!", username); return true; } @@ -51,7 +51,7 @@ public class SshPasswordAuthenticator implements PasswordAuthenticator { username = username.toLowerCase(Locale.US); UserModel user = authManager.authenticate(username, password.toCharArray()); if (user != null) { - client.authenticationSuccess(username); + client.setUser(user); return true; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshSession.java b/src/main/java/com/gitblit/transport/ssh/SshSession.java deleted file mode 100644 index ffff8af4..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshSession.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; - -import org.apache.sshd.common.Session.AttributeKey; - -/** - * - * @author Eric Myrhe - * - */ -public class SshSession { - public static final AttributeKey KEY = - new AttributeKey(); - - private final int sessionId; - private final SocketAddress remoteAddress; - private final String remoteAsString; - - private volatile String username; - private volatile String authError; - private volatile String repositoryName; - - SshSession(int sessionId, SocketAddress peer) { - this.sessionId = sessionId; - this.remoteAddress = peer; - this.remoteAsString = format(remoteAddress); - } - - public SocketAddress getRemoteAddress() { - return remoteAddress; - } - - String getRemoteAddressAsString() { - return remoteAsString; - } - - public String getRemoteUser() { - return username; - } - - /** Unique session number, assigned during connect. */ - public int getSessionId() { - return sessionId; - } - - String getUsername() { - return username; - } - - String getAuthenticationError() { - return authError; - } - - void authenticationSuccess(String user) { - username = user; - authError = null; - } - - void authenticationError(String user, String error) { - username = user; - authError = error; - } - - public void setRepositoryName(String repositoryName) { - this.repositoryName = repositoryName; - } - - public String getRepositoryName() { - return repositoryName; - } - - /** @return {@code true} if the authentication did not succeed. */ - boolean isAuthenticationError() { - return authError != null; - } - - private static String format(final SocketAddress remote) { - if (remote instanceof InetSocketAddress) { - final InetSocketAddress sa = (InetSocketAddress) remote; - - final InetAddress in = sa.getAddress(); - if (in != null) { - return in.getHostAddress(); - } - - final String hostName = sa.getHostName(); - if (hostName != null) { - return hostName; - } - } - return remote.toString(); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java index ae6da3fb..66fe057d 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java @@ -52,21 +52,21 @@ public class SshSessionFactory extends SessionFactory { } } - final GitblitServerSession s = (GitblitServerSession) super + final GitblitServerSession session = (GitblitServerSession) super .createSession(io); SocketAddress peer = io.getRemoteAddress(); - SshSession session = new SshSession(idGenerator.next(), peer); - s.setAttribute(SshSession.KEY, session); + SshDaemonClient client = new SshDaemonClient(peer); + session.setAttribute(SshDaemonClient.KEY, client); // TODO(davido): Log a session close without authentication as a // failure. - s.addCloseSessionListener(new SshFutureListener() { + session.addCloseSessionListener(new SshFutureListener() { @Override public void operationComplete(CloseFuture future) { log.info("connection closed on " + io); } }); - return s; + return session; } @Override diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 31b718e0..dc963309 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -33,7 +33,7 @@ import com.gitblit.git.RepositoryResolver; import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.SshKeyAuthenticator; -import com.gitblit.transport.ssh.SshSession; +import com.gitblit.transport.ssh.SshDaemonClient; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; import com.google.common.base.Strings; @@ -204,18 +204,18 @@ public class DispatchCommand extends BaseCommand { } } - private RepositoryResolver repositoryResolver; - public void setRepositoryResolver(RepositoryResolver repositoryResolver) { + private RepositoryResolver repositoryResolver; + public void setRepositoryResolver(RepositoryResolver repositoryResolver) { this.repositoryResolver = repositoryResolver; } - private GitblitUploadPackFactory gitblitUploadPackFactory; - public void setUploadPackFactory(GitblitUploadPackFactory gitblitUploadPackFactory) { + private GitblitUploadPackFactory gitblitUploadPackFactory; + public void setUploadPackFactory(GitblitUploadPackFactory gitblitUploadPackFactory) { this.gitblitUploadPackFactory = gitblitUploadPackFactory; } - private GitblitReceivePackFactory gitblitReceivePackFactory; - public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { + private GitblitReceivePackFactory gitblitReceivePackFactory; + public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { this.gitblitReceivePackFactory = gitblitReceivePackFactory; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java index dd1e8a06..f8c1334c 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java @@ -25,7 +25,7 @@ public class Receive extends AbstractGitCommand { @Override protected void runImpl() throws Failure { try { - ReceivePack rp = receivePackFactory.create(ctx.getSession(), repo); + ReceivePack rp = receivePackFactory.create(ctx.getClient(), repo); rp.receive(in, out, null); } catch (Exception e) { throw new Failure(1, "fatal: Cannot receive pack: ", e); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java index 44543f42..d1566596 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java @@ -25,7 +25,7 @@ public class Upload extends AbstractGitCommand { @Override protected void runImpl() throws Failure { try { - UploadPack up = uploadPackFactory.create(ctx.getSession(), repo); + UploadPack up = uploadPackFactory.create(ctx.getClient(), repo); up.upload(in, out, null); } catch (Exception e) { throw new Failure(1, "fatal: Cannot upload pack: ", e); -- cgit v1.2.3 From fea7c52e9584ff117be8529b431b40590deef0ca Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:49:50 -0400 Subject: Renamed SshContext->SshCommandContext for clarity of purpose --- .../gitblit/transport/ssh/SshCommandContext.java | 35 ++++++++++++++++++++++ .../gitblit/transport/ssh/SshCommandFactory.java | 2 +- .../java/com/gitblit/transport/ssh/SshContext.java | 35 ---------------------- .../transport/ssh/commands/BaseCommand.java | 6 ++-- 4 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/SshCommandContext.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshContext.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java b/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java new file mode 100644 index 00000000..de79dacc --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +public class SshCommandContext { + + private final SshDaemonClient client; + private final String commandLine; + + public SshCommandContext(SshDaemonClient client, String commandLine) { + this.client = client; + this.commandLine = commandLine; + } + + public SshDaemonClient getClient() { + return client; + } + + public String getCommandLine() { + return commandLine; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index a52e62b8..788bdfb3 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -133,7 +133,7 @@ public class SshCommandFactory implements CommandFactory { private void onStart() throws IOException { synchronized (this) { - SshContext ctx = new SshContext(session.getAttribute(SshDaemonClient.KEY), cmdLine); + SshCommandContext ctx = new SshCommandContext(session.getAttribute(SshDaemonClient.KEY), cmdLine); try { cmd = dispatcher; cmd.setArguments(argv); diff --git a/src/main/java/com/gitblit/transport/ssh/SshContext.java b/src/main/java/com/gitblit/transport/ssh/SshContext.java deleted file mode 100644 index 4c5786e4..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshContext.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -public class SshContext { - - private final SshDaemonClient client; - private final String commandLine; - - public SshContext(SshDaemonClient client, String commandLine) { - this.client = client; - this.commandLine = commandLine; - } - - public SshDaemonClient getClient() { - return client; - } - - public String getCommandLine() { - return commandLine; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index a04c505f..f7ef2aa3 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -33,7 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.transport.ssh.AbstractSshCommand; -import com.gitblit.transport.ssh.SshContext; +import com.gitblit.transport.ssh.SshCommandContext; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.WorkQueue; import com.gitblit.utils.WorkQueue.CancelableRunnable; @@ -52,7 +52,7 @@ public abstract class BaseCommand extends AbstractSshCommand { private String[] argv; /** Ssh context */ - protected SshContext ctx; + protected SshCommandContext ctx; /** The task, as scheduled on a worker thread. */ private final AtomicReference> task; @@ -66,7 +66,7 @@ public abstract class BaseCommand extends AbstractSshCommand { this.executor = w.getDefaultQueue(); } - public void setContext(SshContext ctx) { + public void setContext(SshCommandContext ctx) { this.ctx = ctx; } -- cgit v1.2.3 From 8982e6e0738c6991b9a4b864423bd4f75383c7f4 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:20:45 -0400 Subject: Add add-key and rm-key commands that apply only to the current user --- .../java/com/gitblit/transport/ssh/SshDaemon.java | 18 ++++--- .../transport/ssh/commands/AddKeyCommand.java | 54 +++++++++++++++++++ .../transport/ssh/commands/BaseKeyCommand.java | 58 ++++++++++++++++++++ .../transport/ssh/commands/DispatchCommand.java | 6 +-- .../transport/ssh/commands/RemoveKeyCommand.java | 62 ++++++++++++++++++++++ .../transport/ssh/commands/SetAccountCommand.java | 35 ++---------- 6 files changed, 191 insertions(+), 42 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 81d78784..c8c20f56 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -38,9 +38,11 @@ import com.gitblit.git.GitblitReceivePackFactory; import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; +import com.gitblit.transport.ssh.commands.AddKeyCommand; import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; import com.gitblit.transport.ssh.commands.Receive; +import com.gitblit.transport.ssh.commands.RemoveKeyCommand; import com.gitblit.transport.ssh.commands.ReviewCommand; import com.gitblit.transport.ssh.commands.SetAccountCommand; import com.gitblit.transport.ssh.commands.Upload; @@ -67,7 +69,7 @@ public class SshDaemon { public static enum SshSessionBackend { MINA, NIO2 } - + /** * 22: IANA assigned port number for ssh. Note that this is a distinct * concept from gitblit's default conf for ssh port -- this "default" is @@ -92,7 +94,7 @@ public class SshDaemon { public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) { this.gitblit = gitblit; this.injector = ObjectGraph.create(new SshModule()); - + IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); String bindInterface = settings.getString(Keys.git.sshBindInterface, @@ -107,7 +109,7 @@ public class SshDaemon { backend == SshSessionBackend.MINA ? MinaServiceFactoryFactory.class.getName() : Nio2ServiceFactoryFactory.class.getName()); - + InetSocketAddress addr; if (StringUtils.isEmpty(bindInterface)) { addr = new InetSocketAddress(port); @@ -131,6 +133,8 @@ public class SshDaemon { DispatchCommand gitblitCmd = new DispatchCommand(); gitblitCmd.registerCommand(CreateRepository.class); gitblitCmd.registerCommand(VersionCommand.class); + gitblitCmd.registerCommand(AddKeyCommand.class); + gitblitCmd.registerCommand(RemoveKeyCommand.class); gitblitCmd.registerCommand(SetAccountCommand.class); gitblitCmd.registerCommand(ReviewCommand.class); @@ -210,14 +214,14 @@ public class SshDaemon { } } } - + protected IKeyManager getKeyManager() { IKeyManager keyManager = null; IStoredSettings settings = gitblit.getSettings(); String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName()); if (StringUtils.isEmpty(clazz)) { clazz = FileKeyManager.class.getName(); - } + } try { Class managerClass = (Class) Class.forName(clazz); keyManager = injector.get(managerClass).start(); @@ -232,7 +236,7 @@ public class SshDaemon { } return keyManager; } - + /** * A nested Dagger graph is used for constructor dependency injection of * complex classes. @@ -252,7 +256,7 @@ public class SshDaemon { @Provides @Singleton NullKeyManager provideNullKeyManager() { return new NullKeyManager(); } - + @Provides @Singleton FileKeyManager provideFileKeyManager() { return new FileKeyManager(SshDaemon.this.gitblit); } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java new file mode 100644 index 00000000..69c4fecb --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.IKeyManager; + +/** + * Add a key to the current user's authorized keys list. + * + * @author James Moger + * + */ +@CommandMetaData(name = "add-key", description = "Add an SSH public key to your account") +public class AddKeyCommand extends BaseKeyCommand { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + @Argument(metaVar = "|KEY", usage = "the key to add") + private List addKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + String username = ctx.getClient().getUsername(); + List keys = readKeys(addKeys); + IKeyManager keyManager = authenticator.getKeyManager(); + for (String key : keys) { + keyManager.addKey(username, key); + log.info("added SSH public key for {}", username); + } + authenticator.getKeyCache().invalidate(username); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java new file mode 100644 index 00000000..50927cc2 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.List; + +import com.gitblit.transport.ssh.SshKeyAuthenticator; +import com.google.common.base.Charsets; + +/** + * + * Base class for commands that read SSH keys from stdin or a parameter list. + * + */ +public abstract class BaseKeyCommand extends SshCommand { + + protected List readKeys(List sshKeys) + throws UnsupportedEncodingException, IOException { + int idx = -1; + if (sshKeys.isEmpty() || ((idx = sshKeys.indexOf("-")) >= 0)) { + String sshKey = ""; + BufferedReader br = new BufferedReader(new InputStreamReader( + in, Charsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + sshKey += line + "\n"; + } + if (idx == -1) { + sshKeys.add(sshKey.trim()); + } else { + sshKeys.set(idx, sshKey.trim()); + } + } + return sshKeys; + } + + protected SshKeyAuthenticator authenticator; + public void setAuthenticator(SshKeyAuthenticator authenticator) { + this.authenticator = authenticator; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index dc963309..672ecd61 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -198,9 +198,9 @@ public class DispatchCommand extends BaseCommand { d.setUploadPackFactory(gitblitUploadPackFactory); d.setReceivePackFactory(gitblitReceivePackFactory); d.setAuthenticator(authenticator); - } else if (cmd instanceof SetAccountCommand) { - SetAccountCommand setAccountCommand = (SetAccountCommand)cmd; - setAccountCommand.setAuthenticator(authenticator); + } else if (cmd instanceof BaseKeyCommand) { + BaseKeyCommand k = (BaseKeyCommand)cmd; + k.setAuthenticator(authenticator); } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java new file mode 100644 index 00000000..0d491647 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.IKeyManager; + + +/** + * Remove an SSH public key from the current user's authorized key list. + * + * @author James Moger + * + */ +@CommandMetaData(name = "rm-key", description = "Remove an SSH public key from your account") +public class RemoveKeyCommand extends BaseKeyCommand { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private static final String ALL = "ALL"; + + @Argument(metaVar = "||ALL", usage = "the key to remove") + private List removeKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + String username = ctx.getClient().getUsername(); + List keys = readKeys(removeKeys); + IKeyManager keyManager = authenticator.getKeyManager(); + if (keys.contains(ALL)) { + keyManager.removeAllKeys(username); + log.info("removed all SSH public keys from {}", username); + } else { + for (String key : keys) { + keyManager.removeKey(username, key); + log.info("removed SSH public key from {}", username); + } + } + authenticator.getKeyCache().invalidate(username); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java index 767f3cb4..0eabdce8 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java @@ -14,10 +14,7 @@ package com.gitblit.transport.ssh.commands; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -27,12 +24,10 @@ import org.kohsuke.args4j.Option; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.IKeyManager; -import com.gitblit.transport.ssh.SshKeyAuthenticator; -import com.google.common.base.Charsets; /** Set a user's account settings. **/ @CommandMetaData(name = "set-account", description = "Change an account's settings") -public class SetAccountCommand extends SshCommand { +public class SetAccountCommand extends BaseKeyCommand { private static final String ALL = "ALL"; @@ -61,12 +56,12 @@ public class SetAccountCommand extends SshCommand { } private void setAccount() throws IOException, UnloggedFailure { - addSshKeys = readSshKey(addSshKeys); + addSshKeys = readKeys(addSshKeys); if (!addSshKeys.isEmpty()) { addSshKeys(addSshKeys); } - deleteSshKeys = readSshKey(deleteSshKeys); + deleteSshKeys = readKeys(deleteSshKeys); if (!deleteSshKeys.isEmpty()) { deleteSshKeys(deleteSshKeys); } @@ -91,28 +86,4 @@ public class SetAccountCommand extends SshCommand { } } } - - private List readSshKey(List sshKeys) - throws UnsupportedEncodingException, IOException { - if (!sshKeys.isEmpty()) { - String sshKey; - int idx = sshKeys.indexOf("-"); - if (idx >= 0) { - sshKey = ""; - BufferedReader br = new BufferedReader(new InputStreamReader( - in, Charsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - sshKey += line + "\n"; - } - sshKeys.set(idx, sshKey); - } - } - return sshKeys; - } - - private SshKeyAuthenticator authenticator; - public void setAuthenticator(SshKeyAuthenticator authenticator) { - this.authenticator = authenticator; - } } -- cgit v1.2.3 From 030fd739b3151162c4e84e9c63ce57532af45219 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:28:24 -0400 Subject: Support hidden commands and hide create-repo and review --- src/main/java/com/gitblit/transport/ssh/CommandMetaData.java | 1 + .../com/gitblit/transport/ssh/commands/CreateRepository.java | 2 +- .../java/com/gitblit/transport/ssh/commands/DispatchCommand.java | 5 ++++- .../java/com/gitblit/transport/ssh/commands/ReviewCommand.java | 9 +++++---- 4 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java index 52231b3b..2dd189c1 100644 --- a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java +++ b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java @@ -28,4 +28,5 @@ import java.lang.annotation.Target; public @interface CommandMetaData { String name(); String description() default ""; +boolean hidden() default false; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java index 802905f2..f422b18b 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java @@ -20,7 +20,7 @@ import org.kohsuke.args4j.Option; import com.gitblit.transport.ssh.CommandMetaData; -@CommandMetaData(name = "create-repository", description = "Create new GIT repository") +@CommandMetaData(name = "create-repository", description = "Create new GIT repository", hidden = true) public class CreateRepository extends SshCommand { @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created") diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 672ecd61..83707f77 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -160,9 +160,12 @@ public class DispatchCommand extends BaseCommand { String format = "%-" + maxLength + "s %s"; for (String name : Sets.newTreeSet(m.keySet())) { final Class c = m.get(name); - usage.append(" "); CommandMetaData meta = c.getAnnotation(CommandMetaData.class); if (meta != null) { + if (meta.hidden()) { + continue; + } + usage.append(" "); usage.append(String.format(format, name, Strings.nullToEmpty(meta.description()))); } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java index 9ce73315..b088a2e6 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java @@ -28,7 +28,7 @@ import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.wicket.GitBlitWebSession; -@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets") +@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets", hidden = true) public class ReviewCommand extends SshCommand { private final static short REV_ID_LEN = 40; @@ -58,13 +58,14 @@ public class ReviewCommand extends SshCommand { @Override public void run() throws UnloggedFailure { UserModel user = GitBlitWebSession.get().getUser(); - for (Patchset ps : patchSets) { + // TODO ensure user has permission to score +2/-2 + for (Patchset ps : patchSets) { // review - Change change = new Change(user.username); + Change change = new Change(user.username); change.review(ps, Score.fromScore(vote), false); // TODO(davido): add patchset comment if (submitChange) { - // TODO(davido): merge (when desired and the change is mergeable) + // TODO(davido): merge (when desired and the change is mergeable) } } } -- cgit v1.2.3 From 2331e7936c27e32b7d6d251b3637efc0e1184a81 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:52:50 -0400 Subject: Support admin commands that respect user permissions --- src/main/java/com/gitblit/transport/ssh/CommandMetaData.java | 1 + .../com/gitblit/transport/ssh/commands/CreateRepository.java | 2 +- .../java/com/gitblit/transport/ssh/commands/DispatchCommand.java | 9 +++++++++ .../com/gitblit/transport/ssh/commands/SetAccountCommand.java | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java index 2dd189c1..0d39f33f 100644 --- a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java +++ b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java @@ -28,5 +28,6 @@ import java.lang.annotation.Target; public @interface CommandMetaData { String name(); String description() default ""; +boolean admin() default false; boolean hidden() default false; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java index f422b18b..20f69015 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java @@ -20,7 +20,7 @@ import org.kohsuke.args4j.Option; import com.gitblit.transport.ssh.CommandMetaData; -@CommandMetaData(name = "create-repository", description = "Create new GIT repository", hidden = true) +@CommandMetaData(name = "create-repository", description = "Create new GIT repository", admin = true, hidden = true) public class CreateRepository extends SshCommand { @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created") diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 83707f77..5c022732 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -93,6 +93,12 @@ public class DispatchCommand extends BaseCommand { } Command cmd = getCommand(); + if (cmd.getClass().isAnnotationPresent(CommandMetaData.class)) { + CommandMetaData meta = cmd.getClass().getAnnotation(CommandMetaData.class); + if (meta.admin() && !ctx.getClient().getUser().canAdmin()) { + throw new UnloggedFailure(1, MessageFormat.format("{0} requires admin permissions", commandName)); + } + } if (cmd instanceof BaseCommand) { BaseCommand bc = (BaseCommand) cmd; if (getName().isEmpty()) { @@ -162,6 +168,9 @@ public class DispatchCommand extends BaseCommand { final Class c = m.get(name); CommandMetaData meta = c.getAnnotation(CommandMetaData.class); if (meta != null) { + if (meta.admin() && !ctx.getClient().getUser().canAdmin()) { + continue; + } if (meta.hidden()) { continue; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java index 0eabdce8..a22ca856 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java @@ -26,7 +26,7 @@ import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.IKeyManager; /** Set a user's account settings. **/ -@CommandMetaData(name = "set-account", description = "Change an account's settings") +@CommandMetaData(name = "set-account", description = "Change an account's settings", admin = true) public class SetAccountCommand extends BaseKeyCommand { private static final String ALL = "ALL"; -- cgit v1.2.3 From 22957a70fb7ba8a38564d6b6be15c661da0c3a20 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 12:55:06 -0400 Subject: Start SSH usage documentation --- build.xml | 14 ++++--- src/site/setup_client.mkd | 47 --------------------- src/site/setup_transport_http.mkd | 45 ++++++++++++++++++++ src/site/setup_transport_ssh.mkd | 87 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 53 deletions(-) delete mode 100644 src/site/setup_client.mkd create mode 100644 src/site/setup_transport_http.mkd create mode 100644 src/site/setup_transport_ssh.mkd (limited to 'src') diff --git a/build.xml b/build.xml index 32cb9f58..87f306ab 100644 --- a/build.xml +++ b/build.xml @@ -561,9 +561,10 @@ -

- - + + + + @@ -877,9 +878,10 @@ - - - + + + + diff --git a/src/site/setup_client.mkd b/src/site/setup_client.mkd deleted file mode 100644 index d8fc7d14..00000000 --- a/src/site/setup_client.mkd +++ /dev/null @@ -1,47 +0,0 @@ - -## Client Setup and Configuration -### Https with Self-Signed Certificates -You must tell Git/JGit not to verify the self-signed certificate in order to perform any remote Git operations. - -**NOTE:** -The default self-signed certificate generated by Gitlbit GO is bound to *localhost*. -If you are using Eclipse/EGit/JGit clients, you will have to generate your own certificate that specifies the exact hostname used in your clone/push url. -You must do this because Eclipse/EGit/JGit (< 3.0) always verifies certificate hostnames, regardless of the *http.sslVerify=false* client-side setting. - -- **Eclipse/EGit/JGit** - 1. Window->Preferences->Team->Git->Configuration - 2. Click the *New Entry* button - 3.
Key = http.sslVerify
-Value = false
-- **Command-line Git** ([Git-Config Manual Page](http://www.kernel.org/pub/software/scm/git/docs/git-config.html)) -
git config --global --bool --add http.sslVerify false
- -### Http Post Buffer Size -You may find the default post buffer of your git client is too small to push large deltas to Gitblit. Sometimes this can be observed on your client as *hanging* during a push. Other times it can be observed by git erroring out with a message like: error: RPC failed; result=52, HTTP code = 0. - -This can be adjusted on your client by changing the default post buffer size: -
git config --global http.postBuffer 524288000
- -### Disabling SNI - -You may run into SNI alerts (Server Name Indication). These will manifest as failures to clone or push to your Gitblit instance. - -#### Java-based Clients - -Luckily, Java 6-based clients ignore SNI alerts but when using Java 7-based clients, SNI checking is enabled by default. You can disable SNI alerts by specifying the JVM system parameter `-Djsse.enableSNIExtension=false` when your Java-based client launches. - -For Eclipse, you can append `-Djsse.enableSNIExtension=false` to your *eclipse.ini* file. - -#### Native Clients - -Native clients may display an error when attempting to clone or push that looks like this: - -``` -C:\projects\git\gitblit>git push rhcloud master -error: error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:reason(1112) while accessing https://demo-gitblit.rhcloud.com/git/gitblit.git/info/refs?service=git-receive-pack -fatal: HTTP request failed -``` - -### Cloning a Repository - -Gitblit provides many custom clone links for popular Git clients on the Summary page of each repository. If you have one or more of those clients installed, you should be able to click the link to initiate cloning with the selected tool. diff --git a/src/site/setup_transport_http.mkd b/src/site/setup_transport_http.mkd new file mode 100644 index 00000000..fd611d43 --- /dev/null +++ b/src/site/setup_transport_http.mkd @@ -0,0 +1,45 @@ + +## Using the HTTP/HTTPS transport + +### Https with Self-Signed Certificates +You must tell Git/JGit not to verify the self-signed certificate in order to perform any remote Git operations. + +**NOTE:** +The default self-signed certificate generated by Gitlbit GO is bound to *localhost*. +If you are using Eclipse/EGit/JGit clients, you will have to generate your own certificate that specifies the exact hostname used in your clone/push url. +You must do this because Eclipse/EGit/JGit (< 3.0) always verifies certificate hostnames, regardless of the *http.sslVerify=false* client-side setting. + +- **Eclipse/EGit/JGit** + 1. Window->Preferences->Team->Git->Configuration + 2. Click the *New Entry* button + 3.
Key = http.sslVerify
+Value = false
+- **Command-line Git** ([Git-Config Manual Page](http://www.kernel.org/pub/software/scm/git/docs/git-config.html)) +
git config --global --bool --add http.sslVerify false
+ +### Http Post Buffer Size +You may find the default post buffer of your git client is too small to push large deltas to Gitblit. Sometimes this can be observed on your client as *hanging* during a push. Other times it can be observed by git erroring out with a message like: error: RPC failed; result=52, HTTP code = 0. + +This can be adjusted on your client by changing the default post buffer size: +
git config --global http.postBuffer 524288000
+ +### Disabling SNI + +You may run into SNI alerts (Server Name Indication). These will manifest as failures to clone or push to your Gitblit instance. + +#### Java-based Clients + +Luckily, Java 6-based clients ignore SNI alerts but when using Java 7-based clients, SNI checking is enabled by default. You can disable SNI alerts by specifying the JVM system parameter `-Djsse.enableSNIExtension=false` when your Java-based client launches. + +For Eclipse, you can append `-Djsse.enableSNIExtension=false` to your *eclipse.ini* file. + +#### Native Clients + +Native clients may display an error when attempting to clone or push that looks like this: + +``` +C:\projects\git\gitblit>git push rhcloud master +error: error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:reason(1112) while accessing https://demo-gitblit.rhcloud.com/git/gitblit.git/info/refs?service=git-receive-pack +fatal: HTTP request failed +``` + diff --git a/src/site/setup_transport_ssh.mkd b/src/site/setup_transport_ssh.mkd new file mode 100644 index 00000000..38742ecd --- /dev/null +++ b/src/site/setup_transport_ssh.mkd @@ -0,0 +1,87 @@ + +## Using the SSH transport + +*SINCE 1.5.0* + +The SSH transport is a very exciting improvement to Gitblit. Aside from offering a simple password-less, public key workflow the SSH transport also allows exposes a new approach to interacting with Gitblit: SSH commands. The Gerrit and Android projects have to be thanked for providing great base SSH code that Gitblit has integrated. + +### Cloning & Pushing + +By default, Gitblit serves the SSH transport on port 29418, which is the same as Gerrit. Why was 29418 chosen? It's likely because it resembles the IANA port assigned to the git protocol (9418). + +Gitblit will authenticate using username/password or public keys. + + git clone ssh://@:29418/myrepository.git + +### Setting up your account to use public key authentication + +Public key authentication allows you to operate in a password-less workflow and to separate your web login credentials from your git credentials. Setting up public key authentication is very simple. If you are working on Windows you'll need to install [Git for Windows](http://git-scm.com/download/win). + +First you'll need to create an SSH key pair, if you don't already have one or if you want to generate a new, separate key. + + ssh-keygen + +Then you can upload your *public* key right from the command-line. + + cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 gitblit add-key + cat c:\\.ssh\id_rsa.pub | ssh -l -p 29418 gitblit add-key + +**NOTE:** It is important to note that *ssh-keygen* generates a public/private keypair (e.g. id_rsa and id_rsa.pub). You want to upload the *public* key, which is denoted by the *.pub* file extension. + +Once you've done both of those steps you should be able to execute the following command without a password prompt. + + ssh -l -p 29418 gitblit version + +### Setting up an SSH alias + +Typing the following command syntax all the time gets to be rather tedious. + + ssh -l -p 29418 gitblit version + +You can define an alias for your server which will reduce your command syntax to something like this. + + ssh gitblit version + +Create or modify your `~/.ssh/config` file and add a host entry. If you are on Windows, you'll want to create or modify `\.ssh\config`, where *userfolder* is dependent on your version of Windows. Most recently this is `c:\users\`. + + Host + IdentityFile ~/.ssh/id_rsa + User + Port 29418 + HostName + +### SSH Commands + +#### git + +You will likely never directly interact with the git command, but it is used by your git client to clone, fetch, and push commits to/from your Gitblit server. + +##### git-receive-pack + +This is the command for processing pushes sent from clients. + +##### git-upload-pack + +This is the command for sending refs and commits to clients. + +#### gitblit + +The *gitblit* command has many subcommands for interacting gitblit. + +##### add-key + +Add an SSH public key to your account. This command accepts a public key piped to stdin. + + cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 gitblit add-key + +##### rm-key + +Remove an SSH public key from your account. This command accepts a public key piped to stdin. + + cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 gitblit rm-key + +You can also remove all your public keys from your account. + + ssh -l -p 29418 gitblit rm-key ALL + + -- cgit v1.2.3 From aaecd8f2a36d2c0d780b42425aa57725fe708551 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 14:53:26 -0400 Subject: Move cache to IKeyManager and implement isStale() in FileKeyManager --- .../com/gitblit/transport/ssh/FileKeyManager.java | 52 +++++++++++++--- .../com/gitblit/transport/ssh/IKeyManager.java | 69 +++++++++++++++++----- .../com/gitblit/transport/ssh/NullKeyManager.java | 19 +++--- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 46 ++++----------- .../transport/ssh/commands/AddKeyCommand.java | 1 - .../transport/ssh/commands/RemoveKeyCommand.java | 1 - .../transport/ssh/commands/SetAccountCommand.java | 1 - 7 files changed, 121 insertions(+), 68 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index 1eb470bf..ae0bc9cf 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -21,6 +21,8 @@ import java.security.PublicKey; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.codec.binary.Base64; import org.apache.sshd.common.util.Buffer; @@ -38,12 +40,15 @@ import com.google.common.io.Files; * @author James Moger * */ -public class FileKeyManager implements IKeyManager { +public class FileKeyManager extends IKeyManager { protected final IRuntimeManager runtimeManager; + protected final Map lastModifieds; + public FileKeyManager(IRuntimeManager runtimeManager) { this.runtimeManager = runtimeManager; + this.lastModifieds = new ConcurrentHashMap(); } @Override @@ -68,15 +73,34 @@ public class FileKeyManager implements IKeyManager { } @Override - public List getKeys(String username) { + protected boolean isStale(String username) { + File keystore = getKeystore(username); + if (!keystore.exists()) { + // keystore may have been deleted + return true; + } + + if (lastModifieds.containsKey(keystore)) { + // compare modification times + long lastModified = lastModifieds.get(keystore); + return lastModified != keystore.lastModified(); + } + + // assume stale + return true; + } + + @Override + protected List getKeysImpl(String username) { try { - File keys = getKeystore(username); - if (!keys.exists()) { + log.info("loading keystore for {}", username); + File keystore = getKeystore(username); + if (!keystore.exists()) { return null; } - if (keys.exists()) { + if (keystore.exists()) { List list = new ArrayList(); - for (String entry : Files.readLines(keys, Charsets.ISO_8859_1)) { + for (String entry : Files.readLines(keystore, Charsets.ISO_8859_1)) { if (entry.trim().length() == 0) { // skip blanks continue; @@ -93,6 +117,8 @@ public class FileKeyManager implements IKeyManager { if (list.isEmpty()) { return null; } + + lastModifieds.put(keystore, keystore.lastModified()); return list; } } catch (IOException e) { @@ -140,6 +166,9 @@ public class FileKeyManager implements IKeyManager { // write keystore String content = Joiner.on("\n").join(lines).trim().concat("\n"); Files.write(content, keystore, Charsets.ISO_8859_1); + + lastModifieds.remove(keystore); + keyCache.invalidate(username); return true; } catch (IOException e) { throw new RuntimeException("Cannot add ssh key", e); @@ -183,6 +212,9 @@ public class FileKeyManager implements IKeyManager { String content = Joiner.on("\n").join(lines).trim().concat("\n"); Files.write(content, keystore, Charsets.ISO_8859_1); } + + lastModifieds.remove(keystore); + keyCache.invalidate(username); return true; } } catch (IOException e) { @@ -193,7 +225,13 @@ public class FileKeyManager implements IKeyManager { @Override public boolean removeAllKeys(String username) { - return getKeystore(username).delete(); + File keystore = getKeystore(username); + if (keystore.delete()) { + lastModifieds.remove(keystore); + keyCache.invalidate(username); + return true; + } + return false; } protected File getKeystore(String username) { diff --git a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java index cb32a020..12fce3df 100644 --- a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java @@ -16,26 +16,63 @@ package com.gitblit.transport.ssh; import java.security.PublicKey; +import java.text.MessageFormat; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; /** - * + * * @author James Moger * */ -public interface IKeyManager { - - IKeyManager start(); - - boolean isReady(); - - IKeyManager stop(); - - List getKeys(String username); - - boolean addKey(String username, String data); - - boolean removeKey(String username, String data); - - boolean removeAllKeys(String username); +public abstract class IKeyManager { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final LoadingCache> keyCache = CacheBuilder + .newBuilder(). + expireAfterAccess(15, TimeUnit.MINUTES). + maximumSize(100) + .build(new CacheLoader>() { + @Override + public List load(String username) { + return getKeysImpl(username); + } + }); + + public abstract IKeyManager start(); + + public abstract boolean isReady(); + + public abstract IKeyManager stop(); + + public final List getKeys(String username) { + try { + if (isStale(username)) { + keyCache.invalidate(username); + } + return keyCache.get(username); + } catch (ExecutionException e) { + log.error(MessageFormat.format("failed to retrieve keys for {0}", username), e); + } + return null; + } + + protected abstract boolean isStale(String username); + + protected abstract List getKeysImpl(String username); + + public abstract boolean addKey(String username, String data); + + public abstract boolean removeKey(String username, String data); + + public abstract boolean removeAllKeys(String username); } diff --git a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java index 454d3cfc..c76728d8 100644 --- a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java @@ -20,15 +20,15 @@ import java.util.List; /** * Rejects all SSH key management requests. - * + * * @author James Moger * */ -public class NullKeyManager implements IKeyManager { +public class NullKeyManager extends IKeyManager { public NullKeyManager() { } - + @Override public String toString() { return getClass().getSimpleName(); @@ -38,19 +38,24 @@ public class NullKeyManager implements IKeyManager { public NullKeyManager start() { return this; } - + @Override public boolean isReady() { return true; } - + @Override public NullKeyManager stop() { return this; } @Override - public List getKeys(String username) { + protected boolean isStale(String username) { + return false; + } + + @Override + protected List getKeysImpl(String username) { return null; } @@ -58,7 +63,7 @@ public class NullKeyManager implements IKeyManager { public boolean addKey(String username, String data) { return false; } - + @Override public boolean removeKey(String username, String data) { return false; diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java index 36319226..922f25ae 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java @@ -18,8 +18,6 @@ package com.gitblit.transport.ssh; import java.security.PublicKey; import java.util.List; import java.util.Locale; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.server.session.ServerSession; @@ -28,10 +26,6 @@ import org.slf4j.LoggerFactory; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.UserModel; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; /** * @@ -46,17 +40,6 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { protected final IAuthenticationManager authManager; - LoadingCache> sshKeyCache = CacheBuilder - .newBuilder(). - expireAfterAccess(15, TimeUnit.MINUTES). - maximumSize(100) - .build(new CacheLoader>() { - @Override - public List load(String username) { - return keyManager.getKeys(username); - } - }); - public SshKeyAuthenticator(IKeyManager keyManager, IAuthenticationManager authManager) { this.keyManager = keyManager; this.authManager = authManager; @@ -74,23 +57,20 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { } username = username.toLowerCase(Locale.US); - try { - List keys = sshKeyCache.get(username); - if (keys == null || keys.isEmpty()) { - log.info("{} has not added any public keys for ssh authentication", username); - return false; - } + List keys = keyManager.getKeys(username); + if (keys == null || keys.isEmpty()) { + log.info("{} has not added any public keys for ssh authentication", username); + return false; + } - for (PublicKey key : keys) { - if (key.equals(suppliedKey)) { - UserModel user = authManager.authenticate(username, key); - if (user != null) { - client.setUser(user); - return true; - } + for (PublicKey key : keys) { + if (key.equals(suppliedKey)) { + UserModel user = authManager.authenticate(username, key); + if (user != null) { + client.setUser(user); + return true; } } - } catch (ExecutionException e) { } log.warn("could not authenticate {} for SSH using the supplied public key", username); @@ -100,8 +80,4 @@ public class SshKeyAuthenticator implements PublickeyAuthenticator { public IKeyManager getKeyManager() { return keyManager; } - - public Cache> getKeyCache() { - return sshKeyCache; - } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java index 69c4fecb..35bb1bbf 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java @@ -49,6 +49,5 @@ public class AddKeyCommand extends BaseKeyCommand { keyManager.addKey(username, key); log.info("added SSH public key for {}", username); } - authenticator.getKeyCache().invalidate(username); } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java index 0d491647..90e70418 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java @@ -57,6 +57,5 @@ public class RemoveKeyCommand extends BaseKeyCommand { log.info("removed SSH public key from {}", username); } } - authenticator.getKeyCache().invalidate(username); } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java index a22ca856..1f0d902b 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java @@ -65,7 +65,6 @@ public class SetAccountCommand extends BaseKeyCommand { if (!deleteSshKeys.isEmpty()) { deleteSshKeys(deleteSshKeys); } - authenticator.getKeyCache().invalidate(user); } private void addSshKeys(List sshKeys) throws UnloggedFailure, -- cgit v1.2.3 From 448145827d66cc10326298e60abe53d5b935bbd0 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 14:55:38 -0400 Subject: Rename authenticators for clarity --- .../transport/ssh/PublicKeyAuthenticator.java | 83 ++++++++++++++++++++++ .../java/com/gitblit/transport/ssh/SshDaemon.java | 4 +- .../gitblit/transport/ssh/SshKeyAuthenticator.java | 83 ---------------------- .../transport/ssh/SshPasswordAuthenticator.java | 61 ---------------- .../ssh/UsernamePasswordAuthenticator.java | 61 ++++++++++++++++ .../transport/ssh/commands/BaseKeyCommand.java | 6 +- .../transport/ssh/commands/DispatchCommand.java | 6 +- 7 files changed, 152 insertions(+), 152 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java create mode 100644 src/main/java/com/gitblit/transport/ssh/UsernamePasswordAuthenticator.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java new file mode 100644 index 00000000..84e7afa5 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java @@ -0,0 +1,83 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; +import java.util.List; +import java.util.Locale; + +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.manager.IAuthenticationManager; +import com.gitblit.models.UserModel; + +/** + * + * @author Eric Myrhe + * + */ +public class PublicKeyAuthenticator implements PublickeyAuthenticator { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final IKeyManager keyManager; + + protected final IAuthenticationManager authManager; + + public PublicKeyAuthenticator(IKeyManager keyManager, IAuthenticationManager authManager) { + this.keyManager = keyManager; + this.authManager = authManager; + } + + @Override + public boolean authenticate(String username, final PublicKey suppliedKey, + final ServerSession session) { + final SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); + + if (client.getUser() != null) { + // TODO why do we re-authenticate? + log.info("{} has already authenticated!", username); + return true; + } + + username = username.toLowerCase(Locale.US); + List keys = keyManager.getKeys(username); + if (keys == null || keys.isEmpty()) { + log.info("{} has not added any public keys for ssh authentication", username); + return false; + } + + for (PublicKey key : keys) { + if (key.equals(suppliedKey)) { + UserModel user = authManager.authenticate(username, key); + if (user != null) { + client.setUser(user); + return true; + } + } + } + + log.warn("could not authenticate {} for SSH using the supplied public key", username); + return false; + } + + public IKeyManager getKeyManager() { + return keyManager; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index c8c20f56..5415779e 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -117,7 +117,7 @@ public class SshDaemon { addr = new InetSocketAddress(bindInterface, port); } - SshKeyAuthenticator publickeyAuthenticator = new SshKeyAuthenticator( + PublicKeyAuthenticator publickeyAuthenticator = new PublicKeyAuthenticator( keyManager, gitblit); sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); @@ -125,7 +125,7 @@ public class SshDaemon { sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); sshd.setPublickeyAuthenticator(publickeyAuthenticator); - sshd.setPasswordAuthenticator(new SshPasswordAuthenticator(gitblit)); + sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit)); sshd.setSessionFactory(new SshSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); diff --git a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java deleted file mode 100644 index 922f25ae..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshKeyAuthenticator.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.security.PublicKey; -import java.util.List; -import java.util.Locale; - -import org.apache.sshd.server.PublickeyAuthenticator; -import org.apache.sshd.server.session.ServerSession; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.manager.IAuthenticationManager; -import com.gitblit.models.UserModel; - -/** - * - * @author Eric Myrhe - * - */ -public class SshKeyAuthenticator implements PublickeyAuthenticator { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - protected final IKeyManager keyManager; - - protected final IAuthenticationManager authManager; - - public SshKeyAuthenticator(IKeyManager keyManager, IAuthenticationManager authManager) { - this.keyManager = keyManager; - this.authManager = authManager; - } - - @Override - public boolean authenticate(String username, final PublicKey suppliedKey, - final ServerSession session) { - final SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); - - if (client.getUser() != null) { - // TODO why do we re-authenticate? - log.info("{} has already authenticated!", username); - return true; - } - - username = username.toLowerCase(Locale.US); - List keys = keyManager.getKeys(username); - if (keys == null || keys.isEmpty()) { - log.info("{} has not added any public keys for ssh authentication", username); - return false; - } - - for (PublicKey key : keys) { - if (key.equals(suppliedKey)) { - UserModel user = authManager.authenticate(username, key); - if (user != null) { - client.setUser(user); - return true; - } - } - } - - log.warn("could not authenticate {} for SSH using the supplied public key", username); - return false; - } - - public IKeyManager getKeyManager() { - return keyManager; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java deleted file mode 100644 index 5ddc4a39..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshPasswordAuthenticator.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.util.Locale; - -import org.apache.sshd.server.PasswordAuthenticator; -import org.apache.sshd.server.session.ServerSession; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.manager.IAuthenticationManager; -import com.gitblit.models.UserModel; - -/** - * - * @author James Moger - * - */ -public class SshPasswordAuthenticator implements PasswordAuthenticator { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - protected final IAuthenticationManager authManager; - - public SshPasswordAuthenticator(IAuthenticationManager authManager) { - this.authManager = authManager; - } - - @Override - public boolean authenticate(String username, String password, ServerSession session) { - SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); - if (client.getUser() != null) { - log.info("{} has already authenticated!", username); - return true; - } - - username = username.toLowerCase(Locale.US); - UserModel user = authManager.authenticate(username, password.toCharArray()); - if (user != null) { - client.setUser(user); - return true; - } - - log.warn("could not authenticate {} for SSH using the supplied password", username); - return false; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/UsernamePasswordAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/UsernamePasswordAuthenticator.java new file mode 100644 index 00000000..861bc22d --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/UsernamePasswordAuthenticator.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.util.Locale; + +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.manager.IAuthenticationManager; +import com.gitblit.models.UserModel; + +/** + * + * @author James Moger + * + */ +public class UsernamePasswordAuthenticator implements PasswordAuthenticator { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final IAuthenticationManager authManager; + + public UsernamePasswordAuthenticator(IAuthenticationManager authManager) { + this.authManager = authManager; + } + + @Override + public boolean authenticate(String username, String password, ServerSession session) { + SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); + if (client.getUser() != null) { + log.info("{} has already authenticated!", username); + return true; + } + + username = username.toLowerCase(Locale.US); + UserModel user = authManager.authenticate(username, password.toCharArray()); + if (user != null) { + client.setUser(user); + return true; + } + + log.warn("could not authenticate {} for SSH using the supplied password", username); + return false; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java index 50927cc2..36475244 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java @@ -21,7 +21,7 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.List; -import com.gitblit.transport.ssh.SshKeyAuthenticator; +import com.gitblit.transport.ssh.PublicKeyAuthenticator; import com.google.common.base.Charsets; /** @@ -51,8 +51,8 @@ public abstract class BaseKeyCommand extends SshCommand { return sshKeys; } - protected SshKeyAuthenticator authenticator; - public void setAuthenticator(SshKeyAuthenticator authenticator) { + protected PublicKeyAuthenticator authenticator; + public void setAuthenticator(PublicKeyAuthenticator authenticator) { this.authenticator = authenticator; } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 5c022732..7cd1b045 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -32,7 +32,7 @@ import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.SshKeyAuthenticator; +import com.gitblit.transport.ssh.PublicKeyAuthenticator; import com.gitblit.transport.ssh.SshDaemonClient; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; @@ -231,8 +231,8 @@ public class DispatchCommand extends BaseCommand { this.gitblitReceivePackFactory = gitblitReceivePackFactory; } - private SshKeyAuthenticator authenticator; - public void setAuthenticator(SshKeyAuthenticator authenticator) { + private PublicKeyAuthenticator authenticator; + public void setAuthenticator(PublicKeyAuthenticator authenticator) { this.authenticator = authenticator; } } -- cgit v1.2.3 From 503a853acad49ac6da7f520c26b3b27942dbfec5 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 15:17:00 -0400 Subject: Merge AbstractSshCommand and BaseCommand into a single class --- .../gitblit/transport/ssh/AbstractGitCommand.java | 108 --- .../gitblit/transport/ssh/AbstractSshCommand.java | 84 --- .../transport/ssh/commands/AbstractGitCommand.java | 103 +++ .../transport/ssh/commands/BaseCommand.java | 835 +++++++++++---------- .../transport/ssh/commands/DispatchCommand.java | 1 - .../gitblit/transport/ssh/commands/Receive.java | 1 - .../gitblit/transport/ssh/commands/SshCommand.java | 40 +- .../com/gitblit/transport/ssh/commands/Upload.java | 1 - 8 files changed, 563 insertions(+), 610 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java deleted file mode 100644 index 188cb005..00000000 --- a/src/main/java/com/gitblit/transport/ssh/AbstractGitCommand.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.io.IOException; - -import org.apache.sshd.server.Environment; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import org.kohsuke.args4j.Argument; - -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; -import com.gitblit.transport.ssh.commands.BaseCommand; - -/** - * @author Eric Myhre - * - */ -public abstract class AbstractGitCommand extends BaseCommand { - @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name") - protected String repository; - - protected RepositoryResolver repositoryResolver; - protected ReceivePackFactory receivePackFactory; - protected UploadPackFactory uploadPackFactory; - - protected Repository repo; - - @Override - public void start(final Environment env) { - startThread(new RepositoryCommandRunnable() { - @Override - public void run() throws Exception { - parseCommandLine(); - AbstractGitCommand.this.service(); - } - - @Override - public String getRepository() { - return repository; - } - }); - } - - private void service() throws IOException, Failure { - try { - repo = openRepository(); - runImpl(); - } finally { - if (repo != null) { - repo.close(); - } - } - } - - protected abstract void runImpl() throws IOException, Failure; - - protected Repository openRepository() throws Failure { - // Assume any attempt to use \ was by a Windows client - // and correct to the more typical / used in Git URIs. - // - repository = repository.replace('\\', '/'); - // ssh://git@thishost/path should always be name="/path" here - // - if (!repository.startsWith("/")) { - throw new Failure(1, "fatal: '" + repository - + "': not starts with / character"); - } - repository = repository.substring(1); - try { - return repositoryResolver.open(ctx.getClient(), repository); - } catch (Exception e) { - throw new Failure(1, "fatal: '" + repository - + "': not a git archive", e); - } - } - - public void setRepositoryResolver( - RepositoryResolver repositoryResolver) { - this.repositoryResolver = repositoryResolver; - } - - public void setReceivePackFactory( - GitblitReceivePackFactory receivePackFactory) { - this.receivePackFactory = receivePackFactory; - } - - public void setUploadPackFactory( - GitblitUploadPackFactory uploadPackFactory) { - this.uploadPackFactory = uploadPackFactory; - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java b/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java deleted file mode 100644 index a6681f5c..00000000 --- a/src/main/java/com/gitblit/transport/ssh/AbstractSshCommand.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; - -import org.apache.sshd.server.Command; -import org.apache.sshd.server.Environment; -import org.apache.sshd.server.ExitCallback; -import org.apache.sshd.server.SessionAware; -import org.apache.sshd.server.session.ServerSession; - -import com.google.common.base.Charsets; - -/** - * - * @author Eric Myrhe - * - */ -public abstract class AbstractSshCommand implements Command, SessionAware { - - protected InputStream in; - - protected OutputStream out; - - protected OutputStream err; - - protected ExitCallback exit; - - protected ServerSession session; - - @Override - public void setInputStream(InputStream in) { - this.in = in; - } - - @Override - public void setOutputStream(OutputStream out) { - this.out = out; - } - - @Override - public void setErrorStream(OutputStream err) { - this.err = err; - } - - @Override - public void setExitCallback(ExitCallback exit) { - this.exit = exit; - } - - @Override - public void setSession(final ServerSession session) { - this.session = session; - } - - @Override - public void destroy() {} - - protected static PrintWriter toPrintWriter(final OutputStream o) { - return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8))); - } - - @Override - public abstract void start(Environment env) throws IOException; -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java new file mode 100644 index 00000000..f429b446 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; + +import org.apache.sshd.server.Environment; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.kohsuke.args4j.Argument; + +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.SshDaemonClient; + +/** + * @author Eric Myhre + * + */ +public abstract class AbstractGitCommand extends BaseCommand { + @Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name") + protected String repository; + + protected RepositoryResolver repositoryResolver; + protected ReceivePackFactory receivePackFactory; + protected UploadPackFactory uploadPackFactory; + + protected Repository repo; + + @Override + public void start(final Environment env) { + startThread(new RepositoryCommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + AbstractGitCommand.this.service(); + } + + @Override + public String getRepository() { + return repository; + } + }); + } + + private void service() throws IOException, Failure { + try { + repo = openRepository(); + runImpl(); + } finally { + if (repo != null) { + repo.close(); + } + } + } + + protected abstract void runImpl() throws IOException, Failure; + + protected Repository openRepository() throws Failure { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + repository = repository.replace('\\', '/'); + // ssh://git@thishost/path should always be name="/path" here + // + if (!repository.startsWith("/")) { + throw new Failure(1, "fatal: '" + repository + "': not starts with / character"); + } + repository = repository.substring(1); + try { + return repositoryResolver.open(ctx.getClient(), repository); + } catch (Exception e) { + throw new Failure(1, "fatal: '" + repository + "': not a git archive", e); + } + } + + public void setRepositoryResolver(RepositoryResolver repositoryResolver) { + this.repositoryResolver = repositoryResolver; + } + + public void setReceivePackFactory(GitblitReceivePackFactory receivePackFactory) { + this.receivePackFactory = receivePackFactory; + } + + public void setUploadPackFactory(GitblitUploadPackFactory uploadPackFactory) { + this.uploadPackFactory = uploadPackFactory; + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index f7ef2aa3..baa892ca 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -14,10 +14,13 @@ package com.gitblit.transport.ssh.commands; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.io.StringWriter; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; @@ -26,13 +29,14 @@ import org.apache.sshd.common.SshException; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.transport.ssh.AbstractSshCommand; import com.gitblit.transport.ssh.SshCommandContext; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.WorkQueue; @@ -41,398 +45,439 @@ import com.gitblit.utils.cli.CmdLineParser; import com.google.common.base.Charsets; import com.google.common.util.concurrent.Atomics; -public abstract class BaseCommand extends AbstractSshCommand { - private static final Logger log = LoggerFactory - .getLogger(BaseCommand.class); - - /** Text of the command line which lead up to invoking this instance. */ - private String commandName = ""; - - /** Unparsed command line options. */ - private String[] argv; - - /** Ssh context */ - protected SshCommandContext ctx; - - /** The task, as scheduled on a worker thread. */ - private final AtomicReference> task; - - private final WorkQueue.Executor executor; - - public BaseCommand() { - task = Atomics.newReference(); - IdGenerator gen = new IdGenerator(); - WorkQueue w = new WorkQueue(gen); - this.executor = w.getDefaultQueue(); - } - - public void setContext(SshCommandContext ctx) { - this.ctx = ctx; - } - - public void setInputStream(final InputStream in) { - this.in = in; - } - - public void setOutputStream(final OutputStream out) { - this.out = out; - } - - public void setErrorStream(final OutputStream err) { - this.err = err; - } - - public void setExitCallback(final ExitCallback callback) { - this.exit = callback; - } - - protected void provideBaseStateTo(final Command cmd) { - if (cmd instanceof BaseCommand) { - ((BaseCommand)cmd).setContext(ctx); - } - cmd.setInputStream(in); - cmd.setOutputStream(out); - cmd.setErrorStream(err); - cmd.setExitCallback(exit); - } - - protected String getName() { - return commandName; - } - - void setName(final String prefix) { - this.commandName = prefix; - } - - public String[] getArguments() { - return argv; - } - - public void setArguments(final String[] argv) { - this.argv = argv; - } - - /** - * Parses the command line argument, injecting parsed values into fields. - *

- * This method must be explicitly invoked to cause a parse. - * - * @throws UnloggedFailure if the command line arguments were invalid. - * @see Option - * @see Argument - */ - protected void parseCommandLine() throws UnloggedFailure { - parseCommandLine(this); - } - - /** - * Parses the command line argument, injecting parsed values into fields. - *

- * This method must be explicitly invoked to cause a parse. - * - * @param options object whose fields declare Option and Argument annotations - * to describe the parameters of the command. Usually {@code this}. - * @throws UnloggedFailure if the command line arguments were invalid. - * @see Option - * @see Argument - */ - protected void parseCommandLine(Object options) throws UnloggedFailure { - final CmdLineParser clp = newCmdLineParser(options); - try { - clp.parseArgument(argv); - } catch (IllegalArgumentException err) { - if (!clp.wasHelpRequestedByOption()) { - throw new UnloggedFailure(1, "fatal: " + err.getMessage()); - } - } catch (CmdLineException err) { - if (!clp.wasHelpRequestedByOption()) { - throw new UnloggedFailure(1, "fatal: " + err.getMessage()); - } - } - - if (clp.wasHelpRequestedByOption()) { - StringWriter msg = new StringWriter(); - clp.printDetailedUsage(commandName, msg); - msg.write(usage()); - throw new UnloggedFailure(1, msg.toString()); - } - } - - /** Construct a new parser for this command's received command line. */ - protected CmdLineParser newCmdLineParser(Object options) { - return new CmdLineParser(options); - } - - protected String usage() { - return ""; - } - - private final class TaskThunk implements CancelableRunnable { - private final CommandRunnable thunk; - private final String taskName; - - private TaskThunk(final CommandRunnable thunk) { - this.thunk = thunk; - - StringBuilder m = new StringBuilder(); - m.append(ctx.getCommandLine()); - this.taskName = m.toString(); - } - - @Override - public void cancel() { - synchronized (this) { - try { - //onExit(/*STATUS_CANCEL*/); - } finally { - ctx = null; - } - } - } - - @Override - public void run() { - synchronized (this) { - final Thread thisThread = Thread.currentThread(); - final String thisName = thisThread.getName(); - int rc = 0; - try { - thisThread.setName("SSH " + taskName); - thunk.run(); - - out.flush(); - err.flush(); - } catch (Throwable e) { - try { - out.flush(); - } catch (Throwable e2) { - } - try { - err.flush(); - } catch (Throwable e2) { - } - rc = handleError(e); - } finally { - try { - onExit(rc); - } finally { - thisThread.setName(thisName); - } - } - } - } - - @Override - public String toString() { - return taskName; - } - } - - /** Runnable function which can throw an exception. */ - public static interface CommandRunnable { - public void run() throws Exception; - } - - - /** Runnable function which can retrieve a project name related to the task */ - public static interface RepositoryCommandRunnable extends CommandRunnable { - public String getRepository(); - } - - /** - * Spawn a function into its own thread. - *

- * Typically this should be invoked within {@link Command#start(Environment)}, - * such as: - * - *

-   * startThread(new Runnable() {
-   *   public void run() {
-   *     runImp();
-   *   }
-   * });
-   * 
- * - * @param thunk the runnable to execute on the thread, performing the - * command's logic. - */ - protected void startThread(final Runnable thunk) { - startThread(new CommandRunnable() { - @Override - public void run() throws Exception { - thunk.run(); - } - }); - } - - /** - * Terminate this command and return a result code to the remote client. - *

- * Commands should invoke this at most once. Once invoked, the command may - * lose access to request based resources as any callbacks previously - * registered with {@link RequestCleanup} will fire. - * - * @param rc exit code for the remote client. - */ - protected void onExit(final int rc) { - exit.onExit(rc); -// if (cleanup != null) { -// cleanup.run(); -// } - } - - private int handleError(final Throwable e) { - if ((e.getClass() == IOException.class - && "Pipe closed".equals(e.getMessage())) - || // - (e.getClass() == SshException.class - && "Already closed".equals(e.getMessage())) - || // - e.getClass() == InterruptedIOException.class) { - // This is sshd telling us the client just dropped off while - // we were waiting for a read or a write to complete. Either - // way its not really a fatal error. Don't log it. - // - return 127; - } - - if (e instanceof UnloggedFailure) { - } else { - final StringBuilder m = new StringBuilder(); - m.append("Internal server error"); -// if (userProvider.get().isIdentifiedUser()) { -// final IdentifiedUser u = (IdentifiedUser) userProvider.get(); -// m.append(" (user "); -// m.append(u.getAccount().getUserName()); -// m.append(" account "); -// m.append(u.getAccountId()); -// m.append(")"); -// } -// m.append(" during "); -// m.append(contextProvider.get().getCommandLine()); - log.error(m.toString(), e); - } - - if (e instanceof Failure) { - final Failure f = (Failure) e; - try { - err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8)); - err.flush(); - } catch (IOException e2) { - } catch (Throwable e2) { - log.warn("Cannot send failure message to client", e2); - } - return f.exitCode; - - } else { - try { - err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8)); - err.flush(); - } catch (IOException e2) { - } catch (Throwable e2) { - log.warn("Cannot send internal server error message to client", e2); - } - return 128; - } - } - - /** - * Spawn a function into its own thread. - *

- * Typically this should be invoked within {@link Command#start(Environment)}, - * such as: - * - *

-   * startThread(new CommandRunnable() {
-   *   public void run() throws Exception {
-   *     runImp();
-   *   }
-   * });
-   * 
- *

- * If the function throws an exception, it is translated to a simple message - * for the client, a non-zero exit code, and the stack trace is logged. - * - * @param thunk the runnable to execute on the thread, performing the - * command's logic. - */ - protected void startThread(final CommandRunnable thunk) { - final TaskThunk tt = new TaskThunk(thunk); - task.set(executor.submit(tt)); - } - - /** Thrown from {@link CommandRunnable#run()} with client message and code. */ - public static class Failure extends Exception { - private static final long serialVersionUID = 1L; - - final int exitCode; - - /** - * Create a new failure. - * - * @param exitCode exit code to return the client, which indicates the - * failure status of this command. Should be between 1 and 255, - * inclusive. - * @param msg message to also send to the client's stderr. - */ - public Failure(final int exitCode, final String msg) { - this(exitCode, msg, null); - } - - /** - * Create a new failure. - * - * @param exitCode exit code to return the client, which indicates the - * failure status of this command. Should be between 1 and 255, - * inclusive. - * @param msg message to also send to the client's stderr. - * @param why stack trace to include in the server's log, but is not sent to - * the client's stderr. - */ - public Failure(final int exitCode, final String msg, final Throwable why) { - super(msg, why); - this.exitCode = exitCode; - } - } - - /** Thrown from {@link CommandRunnable#run()} with client message and code. */ - public static class UnloggedFailure extends Failure { - private static final long serialVersionUID = 1L; - - /** - * Create a new failure. - * - * @param msg message to also send to the client's stderr. - */ - public UnloggedFailure(final String msg) { - this(1, msg); - } - - /** - * Create a new failure. - * - * @param exitCode exit code to return the client, which indicates the - * failure status of this command. Should be between 1 and 255, - * inclusive. - * @param msg message to also send to the client's stderr. - */ - public UnloggedFailure(final int exitCode, final String msg) { - this(exitCode, msg, null); - } - - /** - * Create a new failure. - * - * @param exitCode exit code to return the client, which indicates the - * failure status of this command. Should be between 1 and 255, - * inclusive. - * @param msg message to also send to the client's stderr. - * @param why stack trace to include in the server's log, but is not sent to - * the client's stderr. - */ - public UnloggedFailure(final int exitCode, final String msg, - final Throwable why) { - super(exitCode, msg, why); - } - } +public abstract class BaseCommand implements Command, SessionAware { + + private static final Logger log = LoggerFactory.getLogger(BaseCommand.class); + + /** Ssh context */ + protected SshCommandContext ctx; + + protected InputStream in; + + protected OutputStream out; + + protected OutputStream err; + + protected ExitCallback exit; + + protected ServerSession session; + + /** Text of the command line which lead up to invoking this instance. */ + private String commandName = ""; + + /** Unparsed command line options. */ + private String[] argv; + + /** The task, as scheduled on a worker thread. */ + private final AtomicReference> task; + + private final WorkQueue.Executor executor; + + public BaseCommand() { + task = Atomics.newReference(); + IdGenerator gen = new IdGenerator(); + WorkQueue w = new WorkQueue(gen); + this.executor = w.getDefaultQueue(); + } + + @Override + public void setSession(final ServerSession session) { + this.session = session; + } + + @Override + public void destroy() { + } + + protected static PrintWriter toPrintWriter(final OutputStream o) { + return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, Charsets.UTF_8))); + } + + @Override + public abstract void start(Environment env) throws IOException; + + public void setContext(SshCommandContext ctx) { + this.ctx = ctx; + } + + @Override + public void setInputStream(final InputStream in) { + this.in = in; + } + + @Override + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + @Override + public void setErrorStream(final OutputStream err) { + this.err = err; + } + + @Override + public void setExitCallback(final ExitCallback callback) { + this.exit = callback; + } + + protected void provideBaseStateTo(final Command cmd) { + if (cmd instanceof BaseCommand) { + ((BaseCommand) cmd).setContext(ctx); + } + cmd.setInputStream(in); + cmd.setOutputStream(out); + cmd.setErrorStream(err); + cmd.setExitCallback(exit); + } + + protected String getName() { + return commandName; + } + + void setName(final String prefix) { + this.commandName = prefix; + } + + public String[] getArguments() { + return argv; + } + + public void setArguments(final String[] argv) { + this.argv = argv; + } + + /** + * Parses the command line argument, injecting parsed values into fields. + *

+ * This method must be explicitly invoked to cause a parse. + * + * @throws UnloggedFailure + * if the command line arguments were invalid. + * @see Option + * @see Argument + */ + protected void parseCommandLine() throws UnloggedFailure { + parseCommandLine(this); + } + + /** + * Parses the command line argument, injecting parsed values into fields. + *

+ * This method must be explicitly invoked to cause a parse. + * + * @param options + * object whose fields declare Option and Argument annotations to + * describe the parameters of the command. Usually {@code this}. + * @throws UnloggedFailure + * if the command line arguments were invalid. + * @see Option + * @see Argument + */ + protected void parseCommandLine(Object options) throws UnloggedFailure { + final CmdLineParser clp = newCmdLineParser(options); + try { + clp.parseArgument(argv); + } catch (IllegalArgumentException err) { + if (!clp.wasHelpRequestedByOption()) { + throw new UnloggedFailure(1, "fatal: " + err.getMessage()); + } + } catch (CmdLineException err) { + if (!clp.wasHelpRequestedByOption()) { + throw new UnloggedFailure(1, "fatal: " + err.getMessage()); + } + } + + if (clp.wasHelpRequestedByOption()) { + StringWriter msg = new StringWriter(); + clp.printDetailedUsage(commandName, msg); + msg.write(usage()); + throw new UnloggedFailure(1, msg.toString()); + } + } + + /** Construct a new parser for this command's received command line. */ + protected CmdLineParser newCmdLineParser(Object options) { + return new CmdLineParser(options); + } + + protected String usage() { + return ""; + } + + private final class TaskThunk implements CancelableRunnable { + private final CommandRunnable thunk; + private final String taskName; + + private TaskThunk(final CommandRunnable thunk) { + this.thunk = thunk; + + StringBuilder m = new StringBuilder(); + m.append(ctx.getCommandLine()); + this.taskName = m.toString(); + } + + @Override + public void cancel() { + synchronized (this) { + try { + // onExit(/*STATUS_CANCEL*/); + } finally { + ctx = null; + } + } + } + + @Override + public void run() { + synchronized (this) { + final Thread thisThread = Thread.currentThread(); + final String thisName = thisThread.getName(); + int rc = 0; + try { + thisThread.setName("SSH " + taskName); + thunk.run(); + + out.flush(); + err.flush(); + } catch (Throwable e) { + try { + out.flush(); + } catch (Throwable e2) { + } + try { + err.flush(); + } catch (Throwable e2) { + } + rc = handleError(e); + } finally { + try { + onExit(rc); + } finally { + thisThread.setName(thisName); + } + } + } + } + + @Override + public String toString() { + return taskName; + } + } + + /** Runnable function which can throw an exception. */ + public static interface CommandRunnable { + public void run() throws Exception; + } + + /** Runnable function which can retrieve a project name related to the task */ + public static interface RepositoryCommandRunnable extends CommandRunnable { + public String getRepository(); + } + + /** + * Spawn a function into its own thread. + *

+ * Typically this should be invoked within + * {@link Command#start(Environment)}, such as: + * + *

+	 * startThread(new Runnable() {
+	 * 	public void run() {
+	 * 		runImp();
+	 * 	}
+	 * });
+	 * 
+ * + * @param thunk + * the runnable to execute on the thread, performing the + * command's logic. + */ + protected void startThread(final Runnable thunk) { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + thunk.run(); + } + }); + } + + /** + * Terminate this command and return a result code to the remote client. + *

+ * Commands should invoke this at most once. Once invoked, the command may + * lose access to request based resources as any callbacks previously + * registered with {@link RequestCleanup} will fire. + * + * @param rc + * exit code for the remote client. + */ + protected void onExit(final int rc) { + exit.onExit(rc); + // if (cleanup != null) { + // cleanup.run(); + // } + } + + private int handleError(final Throwable e) { + if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) || // + (e.getClass() == SshException.class && "Already closed".equals(e.getMessage())) || // + e.getClass() == InterruptedIOException.class) { + // This is sshd telling us the client just dropped off while + // we were waiting for a read or a write to complete. Either + // way its not really a fatal error. Don't log it. + // + return 127; + } + + if (e instanceof UnloggedFailure) { + } else { + final StringBuilder m = new StringBuilder(); + m.append("Internal server error"); + // if (userProvider.get().isIdentifiedUser()) { + // final IdentifiedUser u = (IdentifiedUser) userProvider.get(); + // m.append(" (user "); + // m.append(u.getAccount().getUserName()); + // m.append(" account "); + // m.append(u.getAccountId()); + // m.append(")"); + // } + // m.append(" during "); + // m.append(contextProvider.get().getCommandLine()); + log.error(m.toString(), e); + } + + if (e instanceof Failure) { + final Failure f = (Failure) e; + try { + err.write((f.getMessage() + "\n").getBytes(Charsets.UTF_8)); + err.flush(); + } catch (IOException e2) { + } catch (Throwable e2) { + log.warn("Cannot send failure message to client", e2); + } + return f.exitCode; + + } else { + try { + err.write("fatal: internal server error\n".getBytes(Charsets.UTF_8)); + err.flush(); + } catch (IOException e2) { + } catch (Throwable e2) { + log.warn("Cannot send internal server error message to client", e2); + } + return 128; + } + } + + /** + * Spawn a function into its own thread. + *

+ * Typically this should be invoked within + * {@link Command#start(Environment)}, such as: + * + *

+	 * startThread(new CommandRunnable() {
+	 * 	public void run() throws Exception {
+	 * 		runImp();
+	 * 	}
+	 * });
+	 * 
+ *

+ * If the function throws an exception, it is translated to a simple message + * for the client, a non-zero exit code, and the stack trace is logged. + * + * @param thunk + * the runnable to execute on the thread, performing the + * command's logic. + */ + protected void startThread(final CommandRunnable thunk) { + final TaskThunk tt = new TaskThunk(thunk); + task.set(executor.submit(tt)); + } + + /** Thrown from {@link CommandRunnable#run()} with client message and code. */ + public static class Failure extends Exception { + private static final long serialVersionUID = 1L; + + final int exitCode; + + /** + * Create a new failure. + * + * @param exitCode + * exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and + * 255, inclusive. + * @param msg + * message to also send to the client's stderr. + */ + public Failure(final int exitCode, final String msg) { + this(exitCode, msg, null); + } + + /** + * Create a new failure. + * + * @param exitCode + * exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and + * 255, inclusive. + * @param msg + * message to also send to the client's stderr. + * @param why + * stack trace to include in the server's log, but is not + * sent to the client's stderr. + */ + public Failure(final int exitCode, final String msg, final Throwable why) { + super(msg, why); + this.exitCode = exitCode; + } + } + + /** Thrown from {@link CommandRunnable#run()} with client message and code. */ + public static class UnloggedFailure extends Failure { + private static final long serialVersionUID = 1L; + + /** + * Create a new failure. + * + * @param msg + * message to also send to the client's stderr. + */ + public UnloggedFailure(final String msg) { + this(1, msg); + } + + /** + * Create a new failure. + * + * @param exitCode + * exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and + * 255, inclusive. + * @param msg + * message to also send to the client's stderr. + */ + public UnloggedFailure(final int exitCode, final String msg) { + this(exitCode, msg, null); + } + + /** + * Create a new failure. + * + * @param exitCode + * exit code to return the client, which indicates the + * failure status of this command. Should be between 1 and + * 255, inclusive. + * @param msg + * message to also send to the client's stderr. + * @param why + * stack trace to include in the server's log, but is not + * sent to the client's stderr. + */ + public UnloggedFailure(final int exitCode, final String msg, final Throwable why) { + super(exitCode, msg, why); + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 7cd1b045..4ace09b3 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -30,7 +30,6 @@ import org.kohsuke.args4j.Argument; import com.gitblit.git.GitblitReceivePackFactory; import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; -import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.PublicKeyAuthenticator; import com.gitblit.transport.ssh.SshDaemonClient; diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java index f8c1334c..5c92a699 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java @@ -17,7 +17,6 @@ package com.gitblit.transport.ssh.commands; import org.eclipse.jgit.transport.ReceivePack; -import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; @CommandMetaData(name = "git-receive-pack", description = "Receive pack") diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java index 44618f3b..ee464e7c 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java @@ -20,26 +20,26 @@ import java.io.PrintWriter; import org.apache.sshd.server.Environment; public abstract class SshCommand extends BaseCommand { - protected PrintWriter stdout; - protected PrintWriter stderr; + protected PrintWriter stdout; + protected PrintWriter stderr; - @Override - public void start(Environment env) throws IOException { - startThread(new CommandRunnable() { - @Override - public void run() throws Exception { - parseCommandLine(); - stdout = toPrintWriter(out); - stderr = toPrintWriter(err); - try { - SshCommand.this.run(); - } finally { - stdout.flush(); - stderr.flush(); - } - } - }); - } + @Override + public void start(Environment env) throws IOException { + startThread(new CommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + stdout = toPrintWriter(out); + stderr = toPrintWriter(err); + try { + SshCommand.this.run(); + } finally { + stdout.flush(); + stderr.flush(); + } + } + }); + } - protected abstract void run() throws UnloggedFailure, Failure, Exception; + protected abstract void run() throws UnloggedFailure, Failure, Exception; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java index d1566596..1607aa6a 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java @@ -17,7 +17,6 @@ package com.gitblit.transport.ssh.commands; import org.eclipse.jgit.transport.UploadPack; -import com.gitblit.transport.ssh.AbstractGitCommand; import com.gitblit.transport.ssh.CommandMetaData; @CommandMetaData(name = "git-upload-pack", description = "Upload pack") -- cgit v1.2.3 From 5891d059a25eddb0238b6389d873d15c67bd7981 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 15:18:24 -0400 Subject: Rename AbstractGitCommand->BaseGitCommand --- .../transport/ssh/commands/AbstractGitCommand.java | 103 --------------------- .../transport/ssh/commands/BaseGitCommand.java | 103 +++++++++++++++++++++ .../transport/ssh/commands/DispatchCommand.java | 4 +- .../gitblit/transport/ssh/commands/Receive.java | 2 +- .../com/gitblit/transport/ssh/commands/Upload.java | 2 +- 5 files changed, 107 insertions(+), 107 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java deleted file mode 100644 index f429b446..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/AbstractGitCommand.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.io.IOException; - -import org.apache.sshd.server.Environment; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import org.kohsuke.args4j.Argument; - -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; -import com.gitblit.transport.ssh.SshDaemonClient; - -/** - * @author Eric Myhre - * - */ -public abstract class AbstractGitCommand extends BaseCommand { - @Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name") - protected String repository; - - protected RepositoryResolver repositoryResolver; - protected ReceivePackFactory receivePackFactory; - protected UploadPackFactory uploadPackFactory; - - protected Repository repo; - - @Override - public void start(final Environment env) { - startThread(new RepositoryCommandRunnable() { - @Override - public void run() throws Exception { - parseCommandLine(); - AbstractGitCommand.this.service(); - } - - @Override - public String getRepository() { - return repository; - } - }); - } - - private void service() throws IOException, Failure { - try { - repo = openRepository(); - runImpl(); - } finally { - if (repo != null) { - repo.close(); - } - } - } - - protected abstract void runImpl() throws IOException, Failure; - - protected Repository openRepository() throws Failure { - // Assume any attempt to use \ was by a Windows client - // and correct to the more typical / used in Git URIs. - // - repository = repository.replace('\\', '/'); - // ssh://git@thishost/path should always be name="/path" here - // - if (!repository.startsWith("/")) { - throw new Failure(1, "fatal: '" + repository + "': not starts with / character"); - } - repository = repository.substring(1); - try { - return repositoryResolver.open(ctx.getClient(), repository); - } catch (Exception e) { - throw new Failure(1, "fatal: '" + repository + "': not a git archive", e); - } - } - - public void setRepositoryResolver(RepositoryResolver repositoryResolver) { - this.repositoryResolver = repositoryResolver; - } - - public void setReceivePackFactory(GitblitReceivePackFactory receivePackFactory) { - this.receivePackFactory = receivePackFactory; - } - - public void setUploadPackFactory(GitblitUploadPackFactory uploadPackFactory) { - this.uploadPackFactory = uploadPackFactory; - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java new file mode 100644 index 00000000..a3411616 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; + +import org.apache.sshd.server.Environment; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.kohsuke.args4j.Argument; + +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.SshDaemonClient; + +/** + * @author Eric Myhre + * + */ +public abstract class BaseGitCommand extends BaseCommand { + @Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name") + protected String repository; + + protected RepositoryResolver repositoryResolver; + protected ReceivePackFactory receivePackFactory; + protected UploadPackFactory uploadPackFactory; + + protected Repository repo; + + @Override + public void start(final Environment env) { + startThread(new RepositoryCommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + BaseGitCommand.this.service(); + } + + @Override + public String getRepository() { + return repository; + } + }); + } + + private void service() throws IOException, Failure { + try { + repo = openRepository(); + runImpl(); + } finally { + if (repo != null) { + repo.close(); + } + } + } + + protected abstract void runImpl() throws IOException, Failure; + + protected Repository openRepository() throws Failure { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + repository = repository.replace('\\', '/'); + // ssh://git@thishost/path should always be name="/path" here + // + if (!repository.startsWith("/")) { + throw new Failure(1, "fatal: '" + repository + "': not starts with / character"); + } + repository = repository.substring(1); + try { + return repositoryResolver.open(ctx.getClient(), repository); + } catch (Exception e) { + throw new Failure(1, "fatal: '" + repository + "': not a git archive", e); + } + } + + public void setRepositoryResolver(RepositoryResolver repositoryResolver) { + this.repositoryResolver = repositoryResolver; + } + + public void setReceivePackFactory(GitblitReceivePackFactory receivePackFactory) { + this.receivePackFactory = receivePackFactory; + } + + public void setUploadPackFactory(GitblitUploadPackFactory uploadPackFactory) { + this.uploadPackFactory = uploadPackFactory; + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 4ace09b3..9ffb1236 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -198,8 +198,8 @@ public class DispatchCommand extends BaseCommand { } private void provideGitState(Command cmd) { - if (cmd instanceof AbstractGitCommand) { - AbstractGitCommand a = (AbstractGitCommand) cmd; + if (cmd instanceof BaseGitCommand) { + BaseGitCommand a = (BaseGitCommand) cmd; a.setRepositoryResolver(repositoryResolver); a.setUploadPackFactory(gitblitUploadPackFactory); a.setReceivePackFactory(gitblitReceivePackFactory); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java index 5c92a699..559cfa5d 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java @@ -20,7 +20,7 @@ import org.eclipse.jgit.transport.ReceivePack; import com.gitblit.transport.ssh.CommandMetaData; @CommandMetaData(name = "git-receive-pack", description = "Receive pack") -public class Receive extends AbstractGitCommand { +public class Receive extends BaseGitCommand { @Override protected void runImpl() throws Failure { try { diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java index 1607aa6a..ac98bb20 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java @@ -20,7 +20,7 @@ import org.eclipse.jgit.transport.UploadPack; import com.gitblit.transport.ssh.CommandMetaData; @CommandMetaData(name = "git-upload-pack", description = "Upload pack") -public class Upload extends AbstractGitCommand { +public class Upload extends BaseGitCommand { @Override protected void runImpl() throws Failure { try { -- cgit v1.2.3 From 61b32fe07e3eb76a7dfbceb66a7758eac5f308ee Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 15:36:55 -0400 Subject: Rename server session classes --- .../transport/ssh/GitblitServerSession.java | 35 ---------- .../java/com/gitblit/transport/ssh/SshDaemon.java | 2 +- .../gitblit/transport/ssh/SshServerSession.java | 34 ++++++++++ .../transport/ssh/SshServerSessionFactory.java | 72 ++++++++++++++++++++ .../gitblit/transport/ssh/SshSessionFactory.java | 77 ---------------------- 5 files changed, 107 insertions(+), 113 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshServerSession.java create mode 100644 src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java b/src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java deleted file mode 100644 index e5336025..00000000 --- a/src/main/java/com/gitblit/transport/ssh/GitblitServerSession.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.future.SshFutureListener; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.server.ServerFactoryManager; -import org.apache.sshd.server.session.ServerSession; - -// Expose addition of close session listeners -class GitblitServerSession extends ServerSession { - - GitblitServerSession(ServerFactoryManager server, IoSession ioSession) - throws Exception { - super(server, ioSession); - } - - void addCloseSessionListener(SshFutureListener l) { - closeFuture.addListener(l); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 5415779e..c11cb1f6 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -126,7 +126,7 @@ public class SshDaemon { gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); sshd.setPublickeyAuthenticator(publickeyAuthenticator); sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit)); - sshd.setSessionFactory(new SshSessionFactory()); + sshd.setSessionFactory(new SshServerSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); diff --git a/src/main/java/com/gitblit/transport/ssh/SshServerSession.java b/src/main/java/com/gitblit/transport/ssh/SshServerSession.java new file mode 100644 index 00000000..d12a6be2 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshServerSession.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.server.ServerFactoryManager; +import org.apache.sshd.server.session.ServerSession; + +// Expose addition of close session listeners +class SshServerSession extends ServerSession { + + SshServerSession(ServerFactoryManager server, IoSession ioSession) throws Exception { + super(server, ioSession); + } + + void addCloseSessionListener(SshFutureListener l) { + closeFuture.addListener(l); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java new file mode 100644 index 00000000..dd3c139d --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.net.SocketAddress; + +import org.apache.mina.transport.socket.SocketSessionConfig; +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.io.IoSession; +import org.apache.sshd.common.io.mina.MinaSession; +import org.apache.sshd.common.session.AbstractSession; +import org.apache.sshd.server.session.SessionFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author James Moger + * + */ +public class SshServerSessionFactory extends SessionFactory { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + public SshServerSessionFactory() { + } + + @Override + protected AbstractSession createSession(final IoSession io) throws Exception { + log.info("connection accepted on " + io); + + if (io instanceof MinaSession) { + if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) { + ((SocketSessionConfig) ((MinaSession) io).getSession().getConfig()).setKeepAlive(true); + } + } + + final SshServerSession session = (SshServerSession) super.createSession(io); + SocketAddress peer = io.getRemoteAddress(); + SshDaemonClient client = new SshDaemonClient(peer); + session.setAttribute(SshDaemonClient.KEY, client); + + // TODO(davido): Log a session close without authentication as a + // failure. + session.addCloseSessionListener(new SshFutureListener() { + @Override + public void operationComplete(CloseFuture future) { + log.info("connection closed on " + io); + } + }); + return session; + } + + @Override + protected AbstractSession doCreateSession(IoSession ioSession) throws Exception { + return new SshServerSession(server, ioSession); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java deleted file mode 100644 index 66fe057d..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshSessionFactory.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.net.SocketAddress; - -import org.apache.mina.transport.socket.SocketSessionConfig; -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.future.SshFutureListener; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.common.io.mina.MinaSession; -import org.apache.sshd.common.session.AbstractSession; -import org.apache.sshd.server.session.SessionFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * - * @author James Moger - * - */ -public class SshSessionFactory extends SessionFactory { - - private final Logger log = LoggerFactory.getLogger(getClass()); - - public SshSessionFactory() { - } - - @Override - protected AbstractSession createSession(final IoSession io) - throws Exception { - log.info("connection accepted on " + io); - - if (io instanceof MinaSession) { - if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) { - ((SocketSessionConfig) ((MinaSession) io).getSession() - .getConfig()).setKeepAlive(true); - } - } - - final GitblitServerSession session = (GitblitServerSession) super - .createSession(io); - SocketAddress peer = io.getRemoteAddress(); - SshDaemonClient client = new SshDaemonClient(peer); - session.setAttribute(SshDaemonClient.KEY, client); - - // TODO(davido): Log a session close without authentication as a - // failure. - session.addCloseSessionListener(new SshFutureListener() { - @Override - public void operationComplete(CloseFuture future) { - log.info("connection closed on " + io); - } - }); - return session; - } - - @Override - protected AbstractSession doCreateSession(IoSession ioSession) - throws Exception { - return new GitblitServerSession(server, ioSession); - } -} -- cgit v1.2.3 From 59e621d541746ff5f2576541abc1a201afcbc15f Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 14 Mar 2014 17:48:23 -0400 Subject: Move dispatcher creation to SshCommandFactory and revise permission checks --- .../gitblit/transport/ssh/SshCommandFactory.java | 461 ++++++++++++--------- .../java/com/gitblit/transport/ssh/SshDaemon.java | 47 +-- .../transport/ssh/commands/BaseCommand.java | 10 - .../transport/ssh/commands/DispatchCommand.java | 392 +++++++++--------- 4 files changed, 460 insertions(+), 450 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index 788bdfb3..da57f76e 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -34,7 +34,21 @@ import org.apache.sshd.server.session.ServerSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.commands.AddKeyCommand; +import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.Receive; +import com.gitblit.transport.ssh.commands.RemoveKeyCommand; +import com.gitblit.transport.ssh.commands.ReviewCommand; +import com.gitblit.transport.ssh.commands.SetAccountCommand; +import com.gitblit.transport.ssh.commands.Upload; +import com.gitblit.transport.ssh.commands.VersionCommand; +import com.gitblit.utils.IdGenerator; import com.gitblit.utils.WorkQueue; import com.google.common.util.concurrent.Atomics; @@ -44,224 +58,261 @@ import com.google.common.util.concurrent.Atomics; * */ public class SshCommandFactory implements CommandFactory { - private static final Logger logger = LoggerFactory - .getLogger(SshCommandFactory.class); - private final ScheduledExecutorService startExecutor; - - private DispatchCommand dispatcher; - - public SshCommandFactory( - WorkQueue workQueue, - DispatchCommand d) { - this.dispatcher = d; - int threads = 2;//cfg.getInt("sshd","commandStartThreads", 2); - startExecutor = workQueue.createQueue(threads, "SshCommandStart"); + private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class); + + private final IGitblit gitblit; + private final PublicKeyAuthenticator keyAuthenticator; + private final ScheduledExecutorService startExecutor; + + public SshCommandFactory(IGitblit gitblit, PublicKeyAuthenticator keyAuthenticator, IdGenerator idGenerator) { + this.gitblit = gitblit; + this.keyAuthenticator = keyAuthenticator; + + int threads = 2;// cfg.getInt("sshd","commandStartThreads", 2); + WorkQueue workQueue = new WorkQueue(idGenerator); + startExecutor = workQueue.createQueue(threads, "SshCommandStart"); + } + + /** + * Creates the root dispatcher command which builds up the available commands. + * + * @param the client + * @param the command line + * @return the root dispatcher command + */ + protected DispatchCommand createRootDispatcher(SshDaemonClient client, String cmdLine) { + final UserModel user = client.getUser(); + + DispatchCommand gitblitCmd = new DispatchCommand(); + gitblitCmd.registerCommand(user, VersionCommand.class); + gitblitCmd.registerCommand(user, AddKeyCommand.class); + gitblitCmd.registerCommand(user, RemoveKeyCommand.class); + gitblitCmd.registerCommand(user, ReviewCommand.class); + + gitblitCmd.registerCommand(user, CreateRepository.class); + gitblitCmd.registerCommand(user, SetAccountCommand.class); + + DispatchCommand gitCmd = new DispatchCommand(); + gitCmd.registerCommand(user, Upload.class); + gitCmd.registerCommand(user, Receive.class); + + DispatchCommand root = new DispatchCommand(); + root.registerDispatcher("gitblit", gitblitCmd); + root.registerDispatcher("git", gitCmd); + + root.setRepositoryResolver(new RepositoryResolver(gitblit)); + root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); + root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); + root.setAuthenticator(keyAuthenticator); + + root.setContext(new SshCommandContext(client, cmdLine)); + + return root; } @Override public Command createCommand(final String commandLine) { - return new Trampoline(commandLine); + return new Trampoline(commandLine); } - private class Trampoline implements Command, SessionAware { - private final String[] argv; - private ServerSession session; - private InputStream in; - private OutputStream out; - private OutputStream err; - private ExitCallback exit; - private Environment env; - private String cmdLine; - private DispatchCommand cmd; - private final AtomicBoolean logged; - private final AtomicReference> task; - - Trampoline(String line) { - if (line.startsWith("git-")) { - line = "git " + line; - } - cmdLine = line; - argv = split(line); - logged = new AtomicBoolean(); - task = Atomics.newReference(); - } - - @Override - public void setSession(ServerSession session) { - this.session = session; - } - - @Override + private class Trampoline implements Command, SessionAware { + private final String[] argv; + private ServerSession session; + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback exit; + private Environment env; + private String cmdLine; + private DispatchCommand cmd; + private final AtomicBoolean logged; + private final AtomicReference> task; + + Trampoline(String line) { + if (line.startsWith("git-")) { + line = "git " + line; + } + cmdLine = line; + argv = split(line); + logged = new AtomicBoolean(); + task = Atomics.newReference(); + } + + @Override + public void setSession(ServerSession session) { + this.session = session; + } + + @Override public void setInputStream(final InputStream in) { - this.in = in; - } + this.in = in; + } - @Override + @Override public void setOutputStream(final OutputStream out) { - this.out = out; - } + this.out = out; + } - @Override + @Override public void setErrorStream(final OutputStream err) { - this.err = err; - } + this.err = err; + } - @Override + @Override public void setExitCallback(final ExitCallback callback) { - this.exit = callback; - } + this.exit = callback; + } - @Override + @Override public void start(final Environment env) throws IOException { - this.env = env; - task.set(startExecutor.submit(new Runnable() { - @Override - public void run() { - try { - onStart(); - } catch (Exception e) { - logger.warn("Cannot start command ", e); - } - } - - @Override - public String toString() { - return "start (user " + session.getUsername() + ")"; - } - })); - } - - private void onStart() throws IOException { - synchronized (this) { - SshCommandContext ctx = new SshCommandContext(session.getAttribute(SshDaemonClient.KEY), cmdLine); - try { - cmd = dispatcher; - cmd.setArguments(argv); - cmd.setContext(ctx); - cmd.setInputStream(in); - cmd.setOutputStream(out); - cmd.setErrorStream(err); - cmd.setExitCallback(new ExitCallback() { - @Override - public void onExit(int rc, String exitMessage) { - exit.onExit(translateExit(rc), exitMessage); - log(rc); - } - - @Override - public void onExit(int rc) { - exit.onExit(translateExit(rc)); - log(rc); - } - }); - cmd.start(env); - } finally { - ctx = null; - } - } - } - - private int translateExit(final int rc) { - return rc; -// -// switch (rc) { -// case BaseCommand.STATUS_NOT_ADMIN: -// return 1; -// -// case BaseCommand.STATUS_CANCEL: -// return 15 /* SIGKILL */; -// -// case BaseCommand.STATUS_NOT_FOUND: -// return 127 /* POSIX not found */; -// -// default: -// return rc; -// } - - } - - private void log(final int rc) { - if (logged.compareAndSet(false, true)) { - //log.onExecute(cmd, rc); - logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc); - } - } - - @Override - public void destroy() { - Future future = task.getAndSet(null); - if (future != null) { - future.cancel(true); -// destroyExecutor.execute(new Runnable() { -// @Override -// public void run() { -// onDestroy(); -// } -// }); - } - } - - private void onDestroy() { - synchronized (this) { - if (cmd != null) { - //final Context old = sshScope.set(ctx); - try { - cmd.destroy(); - //log(BaseCommand.STATUS_CANCEL); - } finally { - //ctx = null; - cmd = null; - //sshScope.set(old); - } - } - } - } - } - - /** Split a command line into a string array. */ - static public String[] split(String commandLine) { - final List list = new ArrayList(); - boolean inquote = false; - boolean inDblQuote = false; - StringBuilder r = new StringBuilder(); - for (int ip = 0; ip < commandLine.length();) { - final char b = commandLine.charAt(ip++); - switch (b) { - case '\t': - case ' ': - if (inquote || inDblQuote) - r.append(b); - else if (r.length() > 0) { - list.add(r.toString()); - r = new StringBuilder(); - } - continue; - case '\"': - if (inquote) - r.append(b); - else - inDblQuote = !inDblQuote; - continue; - case '\'': - if (inDblQuote) - r.append(b); - else - inquote = !inquote; - continue; - case '\\': - if (inquote || ip == commandLine.length()) - r.append(b); // literal within a quote - else - r.append(commandLine.charAt(ip++)); - continue; - default: - r.append(b); - continue; - } - } - if (r.length() > 0) { - list.add(r.toString()); - } - return list.toArray(new String[list.size()]); - } + this.env = env; + task.set(startExecutor.submit(new Runnable() { + @Override + public void run() { + try { + onStart(); + } catch (Exception e) { + logger.warn("Cannot start command ", e); + } + } + + @Override + public String toString() { + return "start (user " + session.getUsername() + ")"; + } + })); + } + + private void onStart() throws IOException { + synchronized (this) { + SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); + try { + cmd = createRootDispatcher(client, cmdLine); + cmd.setArguments(argv); + cmd.setInputStream(in); + cmd.setOutputStream(out); + cmd.setErrorStream(err); + cmd.setExitCallback(new ExitCallback() { + @Override + public void onExit(int rc, String exitMessage) { + exit.onExit(translateExit(rc), exitMessage); + log(rc); + } + + @Override + public void onExit(int rc) { + exit.onExit(translateExit(rc)); + log(rc); + } + }); + cmd.start(env); + } finally { + client = null; + } + } + } + + private int translateExit(final int rc) { + return rc; + // + // switch (rc) { + // case BaseCommand.STATUS_NOT_ADMIN: + // return 1; + // + // case BaseCommand.STATUS_CANCEL: + // return 15 /* SIGKILL */; + // + // case BaseCommand.STATUS_NOT_FOUND: + // return 127 /* POSIX not found */; + // + // default: + // return rc; + // } + + } + + private void log(final int rc) { + if (logged.compareAndSet(false, true)) { + // log.onExecute(cmd, rc); + logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc); + } + } + + @Override + public void destroy() { + Future future = task.getAndSet(null); + if (future != null) { + future.cancel(true); + // destroyExecutor.execute(new Runnable() { + // @Override + // public void run() { + // onDestroy(); + // } + // }); + } + } + + private void onDestroy() { + synchronized (this) { + if (cmd != null) { + // final Context old = sshScope.set(ctx); + try { + cmd.destroy(); + // log(BaseCommand.STATUS_CANCEL); + } finally { + // ctx = null; + cmd = null; + // sshScope.set(old); + } + } + } + } + } + + /** Split a command line into a string array. */ + static public String[] split(String commandLine) { + final List list = new ArrayList(); + boolean inquote = false; + boolean inDblQuote = false; + StringBuilder r = new StringBuilder(); + for (int ip = 0; ip < commandLine.length();) { + final char b = commandLine.charAt(ip++); + switch (b) { + case '\t': + case ' ': + if (inquote || inDblQuote) + r.append(b); + else if (r.length() > 0) { + list.add(r.toString()); + r = new StringBuilder(); + } + continue; + case '\"': + if (inquote) + r.append(b); + else + inDblQuote = !inDblQuote; + continue; + case '\'': + if (inDblQuote) + r.append(b); + else + inquote = !inquote; + continue; + case '\\': + if (inquote || ip == commandLine.length()) + r.append(b); // literal within a quote + else + r.append(commandLine.charAt(ip++)); + continue; + default: + r.append(b); + continue; + } + } + if (r.length() > 0) { + list.add(r.toString()); + } + return list.toArray(new String[list.size()]); + } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index c11cb1f6..c3d48600 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -34,22 +34,9 @@ import org.slf4j.LoggerFactory; import com.gitblit.IStoredSettings; import com.gitblit.Keys; -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; -import com.gitblit.transport.ssh.commands.AddKeyCommand; -import com.gitblit.transport.ssh.commands.CreateRepository; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.Receive; -import com.gitblit.transport.ssh.commands.RemoveKeyCommand; -import com.gitblit.transport.ssh.commands.ReviewCommand; -import com.gitblit.transport.ssh.commands.SetAccountCommand; -import com.gitblit.transport.ssh.commands.Upload; -import com.gitblit.transport.ssh.commands.VersionCommand; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; -import com.gitblit.utils.WorkQueue; import dagger.Module; import dagger.ObjectGraph; @@ -117,45 +104,19 @@ public class SshDaemon { addr = new InetSocketAddress(bindInterface, port); } - PublicKeyAuthenticator publickeyAuthenticator = new PublicKeyAuthenticator( - keyManager, gitblit); + PublicKeyAuthenticator keyAuthenticator = new PublicKeyAuthenticator(keyManager, gitblit); + sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); sshd.setHost(addr.getHostName()); sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); - sshd.setPublickeyAuthenticator(publickeyAuthenticator); + sshd.setPublickeyAuthenticator(keyAuthenticator); sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit)); sshd.setSessionFactory(new SshServerSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); - - DispatchCommand gitblitCmd = new DispatchCommand(); - gitblitCmd.registerCommand(CreateRepository.class); - gitblitCmd.registerCommand(VersionCommand.class); - gitblitCmd.registerCommand(AddKeyCommand.class); - gitblitCmd.registerCommand(RemoveKeyCommand.class); - gitblitCmd.registerCommand(SetAccountCommand.class); - gitblitCmd.registerCommand(ReviewCommand.class); - - DispatchCommand gitCmd = new DispatchCommand(); - gitCmd.registerCommand(Upload.class); - gitCmd.registerCommand(Receive.class); - - DispatchCommand root = new DispatchCommand(); - root.registerDispatcher("gitblit", gitblitCmd); - root.registerDispatcher("git", gitCmd); - - root.setRepositoryResolver(new RepositoryResolver(gitblit)); - root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); - root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); - root.setAuthenticator(publickeyAuthenticator); - - SshCommandFactory commandFactory = new SshCommandFactory( - new WorkQueue(idGenerator), - root); - - sshd.setCommandFactory(commandFactory); + sshd.setCommandFactory(new SshCommandFactory(gitblit, keyAuthenticator, idGenerator)); run = new AtomicBoolean(false); } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index baa892ca..28dfbdd0 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -120,16 +120,6 @@ public abstract class BaseCommand implements Command, SessionAware { this.exit = callback; } - protected void provideBaseStateTo(final Command cmd) { - if (cmd instanceof BaseCommand) { - ((BaseCommand) cmd).setContext(ctx); - } - cmd.setInputStream(in); - cmd.setOutputStream(out); - cmd.setErrorStream(err); - cmd.setExitCallback(exit); - } - protected String getName() { return commandName; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 9ffb1236..673b576e 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -26,10 +26,13 @@ import java.util.Set; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.kohsuke.args4j.Argument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gitblit.git.GitblitReceivePackFactory; import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; +import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.PublicKeyAuthenticator; import com.gitblit.transport.ssh.SshDaemonClient; @@ -41,197 +44,202 @@ import com.google.common.collect.Sets; public class DispatchCommand extends BaseCommand { - @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class) - private String commandName; - - @Argument(index = 1, multiValued = true, metaVar = "ARG") - private List args = new ArrayList(); - - private Set> commands; - private Map> map; - private Map root; - - public DispatchCommand() { - commands = new HashSet>(); - } - - public void registerDispatcher(String name, Command cmd) { - if (root == null) { - root = Maps.newHashMap(); - } - root.put(name, cmd); - } - - public void registerCommand(Class cmd) { - if (!cmd.isAnnotationPresent(CommandMetaData.class)) { - throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", - cmd.getName(), CommandMetaData.class.getName())); - } - commands.add(cmd); - } - - private Map> getMap() { - if (map == null) { - map = Maps.newHashMapWithExpectedSize(commands.size()); - for (Class cmd : commands) { - CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); - map.put(meta.name(), cmd); - } - } - return map; - } - - @Override - public void start(Environment env) throws IOException { - try { - parseCommandLine(); - if (Strings.isNullOrEmpty(commandName)) { - StringWriter msg = new StringWriter(); - msg.write(usage()); - throw new UnloggedFailure(1, msg.toString()); - } - - Command cmd = getCommand(); - if (cmd.getClass().isAnnotationPresent(CommandMetaData.class)) { - CommandMetaData meta = cmd.getClass().getAnnotation(CommandMetaData.class); - if (meta.admin() && !ctx.getClient().getUser().canAdmin()) { - throw new UnloggedFailure(1, MessageFormat.format("{0} requires admin permissions", commandName)); - } - } - if (cmd instanceof BaseCommand) { - BaseCommand bc = (BaseCommand) cmd; - if (getName().isEmpty()) { - bc.setName(commandName); - } else { - bc.setName(getName() + " " + commandName); - } - bc.setArguments(args.toArray(new String[args.size()])); - } - - provideBaseStateTo(cmd); - provideGitState(cmd); - reset(); - //atomicCmd.set(cmd); - cmd.start(env); - - } catch (UnloggedFailure e) { - String msg = e.getMessage(); - if (!msg.endsWith("\n")) { - msg += "\n"; - } - err.write(msg.getBytes(Charsets.UTF_8)); - err.flush(); - exit.onExit(e.exitCode); - } - } - - private Command getCommand() throws UnloggedFailure { - if (root != null && root.containsKey(commandName)) { - return root.get(commandName); + private Logger log = LoggerFactory.getLogger(getClass()); + + @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class) + private String commandName; + + @Argument(index = 1, multiValued = true, metaVar = "ARG") + private List args = new ArrayList(); + + private Set> commands; + private Map> map; + private Map root; + + public DispatchCommand() { + commands = new HashSet>(); + } + + public void registerDispatcher(String name, Command cmd) { + if (root == null) { + root = Maps.newHashMap(); + } + root.put(name, cmd); + } + + /** + * Registers a command as long as the user is permitted to execute it. + * + * @param user + * @param cmd + */ + public void registerCommand(UserModel user, Class cmd) { + if (!cmd.isAnnotationPresent(CommandMetaData.class)) { + throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(), + CommandMetaData.class.getName())); + } + CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); + if (meta.admin() && user.canAdmin()) { + log.debug(MessageFormat.format("excluding admin command {} for {}", meta.name(), user.username)); + return; + } + commands.add(cmd); + } + + private Map> getMap() { + if (map == null) { + map = Maps.newHashMapWithExpectedSize(commands.size()); + for (Class cmd : commands) { + CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); + map.put(meta.name(), cmd); + } + } + return map; + } + + @Override + public void start(Environment env) throws IOException { + try { + parseCommandLine(); + if (Strings.isNullOrEmpty(commandName)) { + StringWriter msg = new StringWriter(); + msg.write(usage()); + throw new UnloggedFailure(1, msg.toString()); + } + + Command cmd = getCommand(); + if (cmd instanceof BaseCommand) { + BaseCommand bc = (BaseCommand) cmd; + if (getName().isEmpty()) { + bc.setName(commandName); + } else { + bc.setName(getName() + " " + commandName); + } + bc.setArguments(args.toArray(new String[args.size()])); + } + + provideStateTo(cmd); + // atomicCmd.set(cmd); + cmd.start(env); + + } catch (UnloggedFailure e) { + String msg = e.getMessage(); + if (!msg.endsWith("\n")) { + msg += "\n"; + } + err.write(msg.getBytes(Charsets.UTF_8)); + err.flush(); + exit.onExit(e.exitCode); + } + } + + private Command getCommand() throws UnloggedFailure { + if (root != null && root.containsKey(commandName)) { + return root.get(commandName); + } + final Class c = getMap().get(commandName); + if (c == null) { + String msg = (getName().isEmpty() ? "Gitblit" : getName()) + ": " + commandName + ": not found"; + throw new UnloggedFailure(1, msg); + } + + Command cmd = null; + try { + cmd = c.newInstance(); + } catch (Exception e) { + throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName)); + } + return cmd; + } + + @Override + protected String usage() { + final StringBuilder usage = new StringBuilder(); + usage.append("Available commands"); + if (!getName().isEmpty()) { + usage.append(" of "); + usage.append(getName()); + } + usage.append(" are:\n"); + usage.append("\n"); + + int maxLength = -1; + Map> m = getMap(); + for (String name : m.keySet()) { + maxLength = Math.max(maxLength, name.length()); + } + String format = "%-" + maxLength + "s %s"; + for (String name : Sets.newTreeSet(m.keySet())) { + final Class c = m.get(name); + CommandMetaData meta = c.getAnnotation(CommandMetaData.class); + if (meta != null) { + if (meta.hidden()) { + continue; + } + usage.append(" "); + usage.append(String.format(format, name, Strings.nullToEmpty(meta.description()))); + } + usage.append("\n"); + } + usage.append("\n"); + + usage.append("See '"); + if (getName().indexOf(' ') < 0) { + usage.append(getName()); + usage.append(' '); + } + usage.append("COMMAND --help' for more information.\n"); + usage.append("\n"); + return usage.toString(); + } + + protected void provideStateTo(final Command cmd) { + if (cmd instanceof BaseCommand) { + ((BaseCommand) cmd).setContext(ctx); + } + cmd.setInputStream(in); + cmd.setOutputStream(out); + cmd.setErrorStream(err); + cmd.setExitCallback(exit); + + if (cmd instanceof BaseGitCommand) { + BaseGitCommand a = (BaseGitCommand) cmd; + a.setRepositoryResolver(repositoryResolver); + a.setUploadPackFactory(gitblitUploadPackFactory); + a.setReceivePackFactory(gitblitReceivePackFactory); + } else if (cmd instanceof DispatchCommand) { + DispatchCommand d = (DispatchCommand) cmd; + d.setRepositoryResolver(repositoryResolver); + d.setUploadPackFactory(gitblitUploadPackFactory); + d.setReceivePackFactory(gitblitReceivePackFactory); + d.setAuthenticator(authenticator); + } else if (cmd instanceof BaseKeyCommand) { + BaseKeyCommand k = (BaseKeyCommand) cmd; + k.setAuthenticator(authenticator); + } + } + + private RepositoryResolver repositoryResolver; + + public void setRepositoryResolver(RepositoryResolver repositoryResolver) { + this.repositoryResolver = repositoryResolver; + } + + private GitblitUploadPackFactory gitblitUploadPackFactory; + + public void setUploadPackFactory(GitblitUploadPackFactory gitblitUploadPackFactory) { + this.gitblitUploadPackFactory = gitblitUploadPackFactory; + } + + private GitblitReceivePackFactory gitblitReceivePackFactory; + + public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { + this.gitblitReceivePackFactory = gitblitReceivePackFactory; + } + + private PublicKeyAuthenticator authenticator; + + public void setAuthenticator(PublicKeyAuthenticator authenticator) { + this.authenticator = authenticator; } - final Class c = getMap().get(commandName); - if (c == null) { - String msg = - (getName().isEmpty() ? "Gitblit" : getName()) + ": " - + commandName + ": not found"; - throw new UnloggedFailure(1, msg); - } - - Command cmd = null; - try { - cmd = c.newInstance(); - } catch (Exception e) { - throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName)); - } - return cmd; - } - - @Override - protected String usage() { - final StringBuilder usage = new StringBuilder(); - usage.append("Available commands"); - if (!getName().isEmpty()) { - usage.append(" of "); - usage.append(getName()); - } - usage.append(" are:\n"); - usage.append("\n"); - - int maxLength = -1; - Map> m = getMap(); - for (String name : m.keySet()) { - maxLength = Math.max(maxLength, name.length()); - } - String format = "%-" + maxLength + "s %s"; - for (String name : Sets.newTreeSet(m.keySet())) { - final Class c = m.get(name); - CommandMetaData meta = c.getAnnotation(CommandMetaData.class); - if (meta != null) { - if (meta.admin() && !ctx.getClient().getUser().canAdmin()) { - continue; - } - if (meta.hidden()) { - continue; - } - usage.append(" "); - usage.append(String.format(format, name, - Strings.nullToEmpty(meta.description()))); - } - usage.append("\n"); - } - usage.append("\n"); - - usage.append("See '"); - if (getName().indexOf(' ') < 0) { - usage.append(getName()); - usage.append(' '); - } - usage.append("COMMAND --help' for more information.\n"); - usage.append("\n"); - return usage.toString(); - } - - // This is needed because we are not using provider or - // clazz.newInstance() for DispatchCommand - private void reset() { - args = new ArrayList(); - } - - private void provideGitState(Command cmd) { - if (cmd instanceof BaseGitCommand) { - BaseGitCommand a = (BaseGitCommand) cmd; - a.setRepositoryResolver(repositoryResolver); - a.setUploadPackFactory(gitblitUploadPackFactory); - a.setReceivePackFactory(gitblitReceivePackFactory); - } else if (cmd instanceof DispatchCommand) { - DispatchCommand d = (DispatchCommand)cmd; - d.setRepositoryResolver(repositoryResolver); - d.setUploadPackFactory(gitblitUploadPackFactory); - d.setReceivePackFactory(gitblitReceivePackFactory); - d.setAuthenticator(authenticator); - } else if (cmd instanceof BaseKeyCommand) { - BaseKeyCommand k = (BaseKeyCommand)cmd; - k.setAuthenticator(authenticator); - } - } - - private RepositoryResolver repositoryResolver; - public void setRepositoryResolver(RepositoryResolver repositoryResolver) { - this.repositoryResolver = repositoryResolver; - } - - private GitblitUploadPackFactory gitblitUploadPackFactory; - public void setUploadPackFactory(GitblitUploadPackFactory gitblitUploadPackFactory) { - this.gitblitUploadPackFactory = gitblitUploadPackFactory; - } - - private GitblitReceivePackFactory gitblitReceivePackFactory; - public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { - this.gitblitReceivePackFactory = gitblitReceivePackFactory; - } - - private PublicKeyAuthenticator authenticator; - public void setAuthenticator(PublicKeyAuthenticator authenticator) { - this.authenticator = authenticator; - } } -- cgit v1.2.3 From 282b8fd82c46ba6874fb24c8715af103645f3406 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Sat, 15 Mar 2014 08:42:26 +0100 Subject: Fix message formatting Change-Id: I1ee1e4135525b7064c4f8e159ed9d8cbef510a81 --- src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 673b576e..1e43e2f2 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -80,7 +80,7 @@ public class DispatchCommand extends BaseCommand { } CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); if (meta.admin() && user.canAdmin()) { - log.debug(MessageFormat.format("excluding admin command {} for {}", meta.name(), user.username)); + log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username)); return; } commands.add(cmd); -- cgit v1.2.3 From 75ebd391b88884581b1139c87c98bb687941a8fe Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Sun, 16 Mar 2014 18:28:03 +0100 Subject: Prevent double authentication for the same public key Openssh client sends two requests, one without a key signature to verify that the public key is acceptable and the second one with the signature after having loaded the private key and signed some data for actual verification. To prevent that the PublickeyAuthenticator#authenticate is called twice cache the authentication status for session and public key. Implement SessionListener to clean up the cache entry when session is destroyed. This is a workaround for SSHD bug [1]. Inspired-By: Guillaume Nodet [1] https://issues.apache.org/jira/browse/SSHD-300 --- .../ssh/CachingPublicKeyAuthenticator.java | 117 +++++++++++++++++++++ .../transport/ssh/PublicKeyAuthenticator.java | 83 --------------- .../gitblit/transport/ssh/SshCommandFactory.java | 7 +- .../java/com/gitblit/transport/ssh/SshDaemon.java | 4 +- .../transport/ssh/commands/BaseKeyCommand.java | 6 +- .../transport/ssh/commands/DispatchCommand.java | 6 +- 6 files changed, 131 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java new file mode 100644 index 00000000..ee1de591 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java @@ -0,0 +1,117 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.sshd.common.Session; +import org.apache.sshd.common.SessionListener; +import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.manager.IAuthenticationManager; +import com.gitblit.models.UserModel; +import com.google.common.base.Preconditions; + +/** + * + * @author Eric Myrhe + * + */ +public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, + SessionListener { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final IKeyManager keyManager; + + protected final IAuthenticationManager authManager; + + private final Map> cache = + new ConcurrentHashMap>(); + + public CachingPublicKeyAuthenticator(IKeyManager keyManager, + IAuthenticationManager authManager) { + this.keyManager = keyManager; + this.authManager = authManager; + } + + @Override + public boolean authenticate(String username, PublicKey key, + ServerSession session) { + Map map = cache.get(session); + if (map == null) { + map = new HashMap(); + cache.put(session, map); + session.addListener(this); + } + if (map.containsKey(key)) { + return map.get(key); + } + boolean result = doAuthenticate(username, key, session); + map.put(key, result); + return result; + } + + private boolean doAuthenticate(String username, PublicKey suppliedKey, + ServerSession session) { + SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); + Preconditions.checkState(client.getUser() == null); + username = username.toLowerCase(Locale.US); + List keys = keyManager.getKeys(username); + if (keys == null || keys.isEmpty()) { + log.info("{} has not added any public keys for ssh authentication", + username); + return false; + } + + for (PublicKey key : keys) { + if (key.equals(suppliedKey)) { + UserModel user = authManager.authenticate(username, key); + if (user != null) { + client.setUser(user); + return true; + } + } + } + + log.warn( + "could not authenticate {} for SSH using the supplied public key", + username); + return false; + } + + public IKeyManager getKeyManager() { + return keyManager; + } + + public void sessionCreated(Session session) { + } + + public void sessionEvent(Session sesssion, Event event) { + } + + public void sessionClosed(Session session) { + cache.remove(session); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java deleted file mode 100644 index 84e7afa5..00000000 --- a/src/main/java/com/gitblit/transport/ssh/PublicKeyAuthenticator.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.security.PublicKey; -import java.util.List; -import java.util.Locale; - -import org.apache.sshd.server.PublickeyAuthenticator; -import org.apache.sshd.server.session.ServerSession; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.manager.IAuthenticationManager; -import com.gitblit.models.UserModel; - -/** - * - * @author Eric Myrhe - * - */ -public class PublicKeyAuthenticator implements PublickeyAuthenticator { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - protected final IKeyManager keyManager; - - protected final IAuthenticationManager authManager; - - public PublicKeyAuthenticator(IKeyManager keyManager, IAuthenticationManager authManager) { - this.keyManager = keyManager; - this.authManager = authManager; - } - - @Override - public boolean authenticate(String username, final PublicKey suppliedKey, - final ServerSession session) { - final SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); - - if (client.getUser() != null) { - // TODO why do we re-authenticate? - log.info("{} has already authenticated!", username); - return true; - } - - username = username.toLowerCase(Locale.US); - List keys = keyManager.getKeys(username); - if (keys == null || keys.isEmpty()) { - log.info("{} has not added any public keys for ssh authentication", username); - return false; - } - - for (PublicKey key : keys) { - if (key.equals(suppliedKey)) { - UserModel user = authManager.authenticate(username, key); - if (user != null) { - client.setUser(user); - return true; - } - } - } - - log.warn("could not authenticate {} for SSH using the supplied public key", username); - return false; - } - - public IKeyManager getKeyManager() { - return keyManager; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index da57f76e..48e8869a 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -61,10 +61,12 @@ public class SshCommandFactory implements CommandFactory { private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class); private final IGitblit gitblit; - private final PublicKeyAuthenticator keyAuthenticator; + private final CachingPublicKeyAuthenticator keyAuthenticator; private final ScheduledExecutorService startExecutor; - public SshCommandFactory(IGitblit gitblit, PublicKeyAuthenticator keyAuthenticator, IdGenerator idGenerator) { + public SshCommandFactory(IGitblit gitblit, + CachingPublicKeyAuthenticator keyAuthenticator, + IdGenerator idGenerator) { this.gitblit = gitblit; this.keyAuthenticator = keyAuthenticator; @@ -252,6 +254,7 @@ public class SshCommandFactory implements CommandFactory { } } + @SuppressWarnings("unused") private void onDestroy() { synchronized (this) { if (cmd != null) { diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index c3d48600..c954b347 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -104,7 +104,8 @@ public class SshDaemon { addr = new InetSocketAddress(bindInterface, port); } - PublicKeyAuthenticator keyAuthenticator = new PublicKeyAuthenticator(keyManager, gitblit); + CachingPublicKeyAuthenticator keyAuthenticator = + new CachingPublicKeyAuthenticator(keyManager, gitblit); sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); @@ -176,6 +177,7 @@ public class SshDaemon { } } + @SuppressWarnings("unchecked") protected IKeyManager getKeyManager() { IKeyManager keyManager = null; IStoredSettings settings = gitblit.getSettings(); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java index 36475244..f92ea6f9 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java @@ -21,7 +21,7 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.List; -import com.gitblit.transport.ssh.PublicKeyAuthenticator; +import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; import com.google.common.base.Charsets; /** @@ -51,8 +51,8 @@ public abstract class BaseKeyCommand extends SshCommand { return sshKeys; } - protected PublicKeyAuthenticator authenticator; - public void setAuthenticator(PublicKeyAuthenticator authenticator) { + protected CachingPublicKeyAuthenticator authenticator; + public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { this.authenticator = authenticator; } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 1e43e2f2..3c041af6 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -34,7 +34,7 @@ import com.gitblit.git.GitblitUploadPackFactory; import com.gitblit.git.RepositoryResolver; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.PublicKeyAuthenticator; +import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; import com.gitblit.transport.ssh.SshDaemonClient; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; @@ -237,9 +237,9 @@ public class DispatchCommand extends BaseCommand { this.gitblitReceivePackFactory = gitblitReceivePackFactory; } - private PublicKeyAuthenticator authenticator; + private CachingPublicKeyAuthenticator authenticator; - public void setAuthenticator(PublicKeyAuthenticator authenticator) { + public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { this.authenticator = authenticator; } } -- cgit v1.2.3 From 5d58a05a9843ec90d06ca42061ff638418f73687 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Sun, 16 Mar 2014 22:55:30 +0100 Subject: Add SSH daemon test --- src/main/distrib/data/gitblit.properties | 5 ++ .../ssh/CachingPublicKeyAuthenticator.java | 2 +- .../java/com/gitblit/transport/ssh/SshDaemon.java | 49 +++++++++++- .../transport/ssh/commands/DispatchCommand.java | 2 +- src/test/config/test-gitblit.properties | 2 + .../gitblit/tests/BogusPublicKeyAuthenticator.java | 39 ++++++++++ src/test/java/com/gitblit/tests/GitBlitSuite.java | 14 +++- src/test/java/com/gitblit/tests/SshDaemonTest.java | 90 ++++++++++++++++++++++ src/test/java/com/gitblit/tests/SshUtils.java | 74 ++++++++++++++++++ 9 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java create mode 100644 src/test/java/com/gitblit/tests/SshDaemonTest.java create mode 100644 src/test/java/com/gitblit/tests/SshUtils.java (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 64a52f5c..52bb252b 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -129,6 +129,11 @@ git.sshKeysFolder= ${baseFolder}/ssh # SINCE 1.5.0 git.sshBackend = NIO2 +# SSH public key authenticator +# +# SINCE 1.5.0 +git.sshPublicKeyAuthenticator = com.gitblit.transport.ssh.CachingPublicKeyAuthenticator + # Allow push/pull over http/https with JGit servlet. # If you do NOT want to allow Git clients to clone/push to Gitblit set this # to false. You might want to do this if you are only using ssh:// or git://. diff --git a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java index ee1de591..7d6066c7 100644 --- a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java @@ -73,7 +73,7 @@ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, return result; } - private boolean doAuthenticate(String username, PublicKey suppliedKey, + protected boolean doAuthenticate(String username, PublicKey suppliedKey, ServerSession session) { SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); Preconditions.checkState(client.getUser() == null); diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index c954b347..40a310e7 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory; import com.gitblit.IStoredSettings; import com.gitblit.Keys; +import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IGitblit; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; @@ -104,8 +105,8 @@ public class SshDaemon { addr = new InetSocketAddress(bindInterface, port); } - CachingPublicKeyAuthenticator keyAuthenticator = - new CachingPublicKeyAuthenticator(keyManager, gitblit); + CachingPublicKeyAuthenticator keyAuthenticator = + getPublicKeyAuthenticator(keyManager, gitblit); sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); @@ -122,6 +123,27 @@ public class SshDaemon { run = new AtomicBoolean(false); } + private CachingPublicKeyAuthenticator getPublicKeyAuthenticator( + IKeyManager keyManager, IGitblit gitblit) { + IStoredSettings settings = gitblit.getSettings(); + String clazz = settings.getString(Keys.git.sshPublicKeyAuthenticator, + CachingPublicKeyAuthenticator.class.getName()); + if (StringUtils.isEmpty(clazz)) { + clazz = CachingPublicKeyAuthenticator.class.getName(); + } + try { + Class authClass = + (Class) Class.forName(clazz); + return authClass.getConstructor( + new Class[] { IKeyManager.class, + IAuthenticationManager.class }).newInstance( + keyManager, gitblit); + } catch (Exception e) { + log.error("failed to create ssh auth manager " + clazz, e); + } + return null; + } + public String formatUrl(String gituser, String servername, String repository) { if (sshd.getPort() == DEFAULT_PORT) { // standard port @@ -200,6 +222,29 @@ public class SshDaemon { return keyManager; } + @SuppressWarnings("unchecked") + protected IKeyManager getKeyAuthenticator() { + IKeyManager keyManager = null; + IStoredSettings settings = gitblit.getSettings(); + String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName()); + if (StringUtils.isEmpty(clazz)) { + clazz = FileKeyManager.class.getName(); + } + try { + Class managerClass = (Class) Class.forName(clazz); + keyManager = injector.get(managerClass).start(); + if (keyManager.isReady()) { + log.info("{} is ready.", keyManager); + } else { + log.warn("{} is disabled.", keyManager); + } + } catch (Exception e) { + log.error("failed to create ssh key manager " + clazz, e); + keyManager = injector.get(NullKeyManager.class).start(); + } + return keyManager; + } + /** * A nested Dagger graph is used for constructor dependency injection of * complex classes. diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 3c041af6..8e13be03 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -79,7 +79,7 @@ public class DispatchCommand extends BaseCommand { CommandMetaData.class.getName())); } CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); - if (meta.admin() && user.canAdmin()) { + if (meta.admin() && user != null && user.canAdmin()) { log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username)); return; } diff --git a/src/test/config/test-gitblit.properties b/src/test/config/test-gitblit.properties index e636469e..7d8e9a79 100644 --- a/src/test/config/test-gitblit.properties +++ b/src/test/config/test-gitblit.properties @@ -7,6 +7,8 @@ git.repositoriesFolder = ${baseFolder}/git git.searchRepositoriesSubfolders = true git.enableGitServlet = true git.daemonPort = 8300 +git.sshPort = 29418 +git.sshPublicKeyAuthenticator = com.gitblit.tests.BogusPublicKeyAuthenticator groovy.scriptsFolder = src/main/distrib/data/groovy groovy.preReceiveScripts = blockpush groovy.postReceiveScripts = sendmail diff --git a/src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java b/src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java new file mode 100644 index 00000000..80be1a01 --- /dev/null +++ b/src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.tests; + +import java.security.PublicKey; + +import org.apache.sshd.server.session.ServerSession; + +import com.gitblit.manager.IAuthenticationManager; +import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; +import com.gitblit.transport.ssh.IKeyManager; + +public class BogusPublicKeyAuthenticator extends CachingPublicKeyAuthenticator { + + public BogusPublicKeyAuthenticator(IKeyManager keyManager, + IAuthenticationManager authManager) { + super(keyManager, authManager); + } + + @Override + protected boolean doAuthenticate(String username, PublicKey suppliedKey, + ServerSession session) { + // TODO(davido): put authenticated user in session + return true; + } +} diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java index c015c847..17d609e7 100644 --- a/src/test/java/com/gitblit/tests/GitBlitSuite.java +++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java @@ -61,7 +61,7 @@ import com.gitblit.utils.JGitUtils; MarkdownUtilsTest.class, JGitUtilsTest.class, SyndicationUtilsTest.class, DiffUtilsTest.class, MetricUtilsTest.class, X509UtilsTest.class, GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class, - GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class, + GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class, SshDaemonTest.class, FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class, ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class, BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class }) @@ -78,6 +78,16 @@ public class GitBlitSuite { static int port = 8280; static int gitPort = 8300; static int shutdownPort = 8281; + static int sshPort = 29418; + +// Overriding of keys doesn't seem to work +// static { +// try { +// sshPort = SshUtils.getFreePort(); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } public static String url = "http://localhost:" + port; public static String gitServletUrl = "http://localhost:" + port + "/git"; @@ -140,6 +150,8 @@ public class GitBlitSuite { "\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"", "--userService", GitBlitSuite.USERSCONF.getAbsolutePath(), "--settings", GitBlitSuite.SETTINGS.getAbsolutePath(), "--baseFolder", "data"); + // doesn't work + //, "--sshPort", "" + sshPort); } }); diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java new file mode 100644 index 00000000..5294f691 --- /dev/null +++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.tests; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.security.KeyPair; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.common.KeyPairProvider; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.gitblit.Constants; + +public class SshDaemonTest extends GitblitUnitTest { + + private static final AtomicBoolean started = new AtomicBoolean(false); + private static KeyPair pair; + + @BeforeClass + public static void startGitblit() throws Exception { + started.set(GitBlitSuite.startGitblit()); + pair = SshUtils.createTestHostKeyProvider().loadKey(KeyPairProvider.SSH_RSA); + } + + @AfterClass + public static void stopGitblit() throws Exception { + if (started.get()) { + GitBlitSuite.stopGitblit(); + } + } + + @Test + public void testPublicKeyAuthentication() throws Exception { + SshClient client = SshClient.setUpDefaultClient(); + client.start(); + ClientSession session = client.connect("localhost", GitBlitSuite.sshPort).await().getSession(); + pair.getPublic().getEncoded(); + assertTrue(session.authPublicKey("admin", pair).await().isSuccess()); + } + + @Test + public void testVersionCommand() throws Exception { + SshClient client = SshClient.setUpDefaultClient(); + client.start(); + ClientSession session = client.connect("localhost", GitBlitSuite.sshPort).await().getSession(); + pair.getPublic().getEncoded(); + assertTrue(session.authPublicKey("admin", pair).await().isSuccess()); + + ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, "gitblit version"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Writer w = new OutputStreamWriter(baos); + w.close(); + channel.setIn(new ByteArrayInputStream(baos.toByteArray())); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + channel.setOut(out); + channel.setErr(err); + channel.open(); + + channel.waitFor(ClientChannel.CLOSED, 0); + + String result = out.toString().trim(); + channel.close(false); + client.stop(); + + assertEquals(Constants.getGitBlitVersion(), result); + } +} diff --git a/src/test/java/com/gitblit/tests/SshUtils.java b/src/test/java/com/gitblit/tests/SshUtils.java new file mode 100644 index 00000000..9760f755 --- /dev/null +++ b/src/test/java/com/gitblit/tests/SshUtils.java @@ -0,0 +1,74 @@ +/* + * 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 com.gitblit.tests; + +import java.io.File; +import java.net.ServerSocket; +import java.net.URISyntaxException; +import java.net.URL; + +import org.apache.sshd.common.KeyPairProvider; +import org.apache.sshd.common.keyprovider.FileKeyPairProvider; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; + +public class SshUtils { + + public static KeyPairProvider createTestHostKeyProvider() { + return new SimpleGeneratorHostKeyProvider("target/hostkey.rsa", "RSA"); + } + + public static FileKeyPairProvider createTestKeyPairProvider(String resource) { + return new FileKeyPairProvider(new String[] { getFile(resource) }); + } + + public static int getFreePort() throws Exception { + ServerSocket s = new ServerSocket(0); + try { + return s.getLocalPort(); + } finally { + s.close(); + } + } + + private static String getFile(String resource) { + URL url = SshUtils.class.getClassLoader().getResource(resource); + File f; + try { + f = new File(url.toURI()); + } catch(URISyntaxException e) { + f = new File(url.getPath()); + } + return f.toString(); + } + + public static void deleteRecursive(File file) { + if (file != null) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursive(child); + } + } + } + file.delete(); + } + } + +} -- cgit v1.2.3 From e725e16eaf7b7ddc1608b1038ade19c26b5b2048 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 16 Mar 2014 15:17:58 -0400 Subject: Add a welcome shell Conflicts: src/main/java/com/gitblit/transport/ssh/SshDaemon.java --- .../java/com/gitblit/transport/ssh/SshDaemon.java | 9 +- .../com/gitblit/transport/ssh/WelcomeShell.java | 156 +++++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/gitblit/transport/ssh/WelcomeShell.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index 40a310e7..b6c5d680 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -32,6 +32,7 @@ import org.eclipse.jgit.internal.JGitText; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.manager.IAuthenticationManager; @@ -105,20 +106,24 @@ public class SshDaemon { addr = new InetSocketAddress(bindInterface, port); } + File hostKeyStore = new File(gitblit.getBaseFolder(), HOST_KEY_STORE); CachingPublicKeyAuthenticator keyAuthenticator = getPublicKeyAuthenticator(keyManager, gitblit); sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); sshd.setHost(addr.getHostName()); - sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(new File( - gitblit.getBaseFolder(), HOST_KEY_STORE).getPath())); + sshd.setKeyPairProvider(new PEMGeneratorHostKeyProvider(hostKeyStore.getPath())); sshd.setPublickeyAuthenticator(keyAuthenticator); sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit)); sshd.setSessionFactory(new SshServerSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); sshd.setCommandFactory(new SshCommandFactory(gitblit, keyAuthenticator, idGenerator)); + sshd.setShellFactory(new WelcomeShell(settings)); + + String version = Constants.getGitBlitVersion() + " (" + sshd.getVersion() + ")"; + sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version); run = new AtomicBoolean(false); } diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java new file mode 100644 index 00000000..156e99e4 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java @@ -0,0 +1,156 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.MessageFormat; + +import org.apache.sshd.common.Factory; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.util.SystemReader; + +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.models.UserModel; +import com.gitblit.utils.StringUtils; + +/** + * Class that displays a welcome message for any shell requests. + * + */ +public class WelcomeShell implements Factory { + + private final IStoredSettings settings; + + public WelcomeShell(IStoredSettings settings) { + this.settings = settings; + } + + @Override + public Command create() { + return new SendMessage(settings); + } + + private static class SendMessage implements Command, SessionAware { + + private final IStoredSettings settings; + private SshDaemonClient client; + + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback exit; + + SendMessage(IStoredSettings settings) { + this.settings = settings; + } + + @Override + public void setInputStream(final InputStream in) { + this.in = in; + } + + @Override + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + @Override + public void setErrorStream(final OutputStream err) { + this.err = err; + } + + @Override + public void setExitCallback(final ExitCallback callback) { + this.exit = callback; + } + + @Override + public void setSession(final ServerSession session) { + this.client = session.getAttribute(SshDaemonClient.KEY); + } + + @Override + public void start(final Environment env) throws IOException { + err.write(Constants.encode(getMessage())); + err.flush(); + + in.close(); + out.close(); + err.close(); + exit.onExit(127); + } + + @Override + public void destroy() { + } + + String getMessage() { + UserModel user = client.getUser(); + + StringBuilder msg = new StringBuilder(); + msg.append("\r\n"); + msg.append(" Hi "); + msg.append(user.getDisplayName()); + msg.append(", you have successfully connected to Gitblit over SSH."); + msg.append("\r\n"); + msg.append("\r\n"); + + msg.append(" You may clone a repository with the following Git syntax:\r\n"); + msg.append("\r\n"); + + msg.append(" git clone "); + msg.append(formatUrl(user.username)); + msg.append("\r\n"); + msg.append("\r\n"); + + return msg.toString(); + } + + private String formatUrl(String username) { + String host = null; + String url = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443"); + if (url != null) { + try { + host = new URL(url).getHost(); + } catch (MalformedURLException e) { + } + } + if (StringUtils.isEmpty(host)) { + host = SystemReader.getInstance().getHostname(); + } + + int port = settings.getInteger(Keys.git.sshPort, 0); + if (port == 22) { + // standard port + return MessageFormat.format("{0}@{1}/REPOSITORY.git", username, host); + } else { + // non-standard port + return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/REPOSITORY.git", + username, host, port); + } + } + } +} -- cgit v1.2.3 From 0d232164930581ccc9eac1b54e4c624a62f107bd Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 16 Mar 2014 16:02:57 -0400 Subject: Pass IGitblit into the SSH command context --- src/main/java/com/gitblit/transport/ssh/SshCommandContext.java | 10 +++++++++- src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java | 2 +- .../java/com/gitblit/transport/ssh/commands/BaseCommand.java | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java b/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java index de79dacc..163d0795 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java @@ -15,16 +15,24 @@ */ package com.gitblit.transport.ssh; +import com.gitblit.manager.IGitblit; + public class SshCommandContext { + private final IGitblit gitblit; private final SshDaemonClient client; private final String commandLine; - public SshCommandContext(SshDaemonClient client, String commandLine) { + public SshCommandContext(IGitblit gitblit, SshDaemonClient client, String commandLine) { + this.gitblit = gitblit; this.client = client; this.commandLine = commandLine; } + public IGitblit getGitblit() { + return gitblit; + } + public SshDaemonClient getClient() { return client; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index 48e8869a..df288db0 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -107,7 +107,7 @@ public class SshCommandFactory implements CommandFactory { root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); root.setAuthenticator(keyAuthenticator); - root.setContext(new SshCommandContext(client, cmdLine)); + root.setContext(new SshCommandContext(gitblit, client, cmdLine)); return root; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index 28dfbdd0..fb2d369f 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -87,6 +87,7 @@ public abstract class BaseCommand implements Command, SessionAware { @Override public void destroy() { + ctx = null; } protected static PrintWriter toPrintWriter(final OutputStream o) { -- cgit v1.2.3 From b2ef03a213de13fe18874781458c8391aa6586f9 Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 16 Mar 2014 16:04:07 -0400 Subject: Add a simple repository list command --- .../gitblit/transport/ssh/SshCommandFactory.java | 2 + .../ssh/commands/ListRepositoriesCommand.java | 72 ++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index df288db0..b8dd5b9f 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -42,6 +42,7 @@ import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.commands.AddKeyCommand; import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.ListRepositoriesCommand; import com.gitblit.transport.ssh.commands.Receive; import com.gitblit.transport.ssh.commands.RemoveKeyCommand; import com.gitblit.transport.ssh.commands.ReviewCommand; @@ -89,6 +90,7 @@ public class SshCommandFactory implements CommandFactory { gitblitCmd.registerCommand(user, VersionCommand.class); gitblitCmd.registerCommand(user, AddKeyCommand.class); gitblitCmd.registerCommand(user, RemoveKeyCommand.class); + gitblitCmd.registerCommand(user, ListRepositoriesCommand.class); gitblitCmd.registerCommand(user, ReviewCommand.class); gitblitCmd.registerCommand(user, CreateRepository.class); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java new file mode 100644 index 00000000..7c58e7fc --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.List; + +import org.kohsuke.args4j.Option; +import org.parboiled.common.StringUtils; + +import com.gitblit.manager.IGitblit; +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; + +@CommandMetaData(name = "repositories", description = "List the available repositories") +public class ListRepositoriesCommand extends SshCommand { + + @Option(name = "--verbose", aliases = { "-v" }, usage = "verbose") + private boolean verbose; + + @Override + public void run() { + IGitblit gitblit = ctx.getGitblit(); + UserModel user = ctx.getClient().getUser(); + List repositories = gitblit.getRepositoryModels(user); + int nameLen = 0; + int descLen = 0; + for (RepositoryModel repo : repositories) { + int len = repo.name.length(); + if (len > nameLen) { + nameLen = len; + } + if (!StringUtils.isEmpty(repo.description)) { + len = repo.description.length(); + if (len > descLen) { + descLen = len; + } + } + } + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + + String pattern; + if (verbose) { + pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen); + } else { + pattern = "%s"; + } + + for (RepositoryModel repo : repositories) { + stdout.println(String.format(pattern, + repo.name, + repo.description == null ? "" : repo.description, + df.format(repo.lastChange))); + } + } +} -- cgit v1.2.3 From 01b529edac600feb3c0750d27d0759b5f49fde72 Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 17 Mar 2014 21:07:49 -0400 Subject: Show root commands in welcome shell and improve command registration --- .../com/gitblit/transport/ssh/WelcomeShell.java | 24 +++- .../transport/ssh/commands/BaseCommand.java | 2 +- .../transport/ssh/commands/DispatchCommand.java | 125 +++++++++++++-------- 3 files changed, 99 insertions(+), 52 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java index 156e99e4..819028c5 100644 --- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java +++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java @@ -34,6 +34,7 @@ import org.eclipse.jgit.util.SystemReader; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.commands.DispatchCommand; import com.gitblit.utils.StringUtils; /** @@ -56,7 +57,7 @@ public class WelcomeShell implements Factory { private static class SendMessage implements Command, SessionAware { private final IStoredSettings settings; - private SshDaemonClient client; + private ServerSession session; private InputStream in; private OutputStream out; @@ -89,7 +90,7 @@ public class WelcomeShell implements Factory { @Override public void setSession(final ServerSession session) { - this.client = session.getAttribute(SshDaemonClient.KEY); + this.session = session; } @Override @@ -105,27 +106,38 @@ public class WelcomeShell implements Factory { @Override public void destroy() { + this.session = null; } String getMessage() { + SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); UserModel user = client.getUser(); StringBuilder msg = new StringBuilder(); msg.append("\r\n"); - msg.append(" Hi "); + msg.append("Hi "); msg.append(user.getDisplayName()); - msg.append(", you have successfully connected to Gitblit over SSH."); + msg.append(", you have successfully connected to Gitblit over SSH"); + msg.append("\r\n"); + msg.append("with client: "); + msg.append(session.getClientVersion()); msg.append("\r\n"); msg.append("\r\n"); - msg.append(" You may clone a repository with the following Git syntax:\r\n"); + msg.append("You may clone a repository with the following Git syntax:\r\n"); msg.append("\r\n"); - msg.append(" git clone "); + msg.append(" git clone "); msg.append(formatUrl(user.username)); msg.append("\r\n"); msg.append("\r\n"); + // display the core commands + SshCommandFactory cmdFactory = (SshCommandFactory) session.getFactoryManager().getCommandFactory(); + DispatchCommand root = cmdFactory.createRootDispatcher(client, ""); + String usage = root.usage().replace("\n", "\r\n"); + msg.append(usage); + return msg.toString(); } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index fb2d369f..a3df787c 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -191,7 +191,7 @@ public abstract class BaseCommand implements Command, SessionAware { return new CmdLineParser(options); } - protected String usage() { + public String usage() { return ""; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 8e13be03..00d3b819 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -22,6 +22,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; @@ -36,6 +37,7 @@ import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; import com.gitblit.transport.ssh.SshDaemonClient; +mport com.gitblit.utils.StringUtils; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; import com.google.common.base.Strings; @@ -52,47 +54,53 @@ public class DispatchCommand extends BaseCommand { @Argument(index = 1, multiValued = true, metaVar = "ARG") private List args = new ArrayList(); - private Set> commands; - private Map> map; - private Map root; + private Set> commands; + private Map> map; + private Map dispatchers; public DispatchCommand() { - commands = new HashSet>(); + commands = new HashSet>(); } public void registerDispatcher(String name, Command cmd) { - if (root == null) { - root = Maps.newHashMap(); + if (dispatchers == null) { + dispatchers = Maps.newHashMap(); } - root.put(name, cmd); + dispatchers.put(name, cmd); } + /** * Registers a command as long as the user is permitted to execute it. * * @param user * @param cmd */ - public void registerCommand(UserModel user, Class cmd) { + public void registerCommand(UserModel user, Class cmd) { if (!cmd.isAnnotationPresent(CommandMetaData.class)) { throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(), CommandMetaData.class.getName())); } CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); - if (meta.admin() && user != null && user.canAdmin()) { + if (meta.admin() && !user.canAdmin()) { log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username)); return; } commands.add(cmd); } - private Map> getMap() { + private Map> getMap() { if (map == null) { map = Maps.newHashMapWithExpectedSize(commands.size()); - for (Class cmd : commands) { + for (Class cmd : commands) { CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); map.put(meta.name(), cmd); } + if (dispatchers != null) { + for (Map.Entry entry : dispatchers.entrySet()) { + map.put(entry.getKey(), entry.getValue().getClass()); + } + } } return map; } @@ -107,16 +115,13 @@ public class DispatchCommand extends BaseCommand { throw new UnloggedFailure(1, msg.toString()); } - Command cmd = getCommand(); - if (cmd instanceof BaseCommand) { - BaseCommand bc = (BaseCommand) cmd; - if (getName().isEmpty()) { - bc.setName(commandName); - } else { - bc.setName(getName() + " " + commandName); - } - bc.setArguments(args.toArray(new String[args.size()])); + BaseCommand cmd = getCommand(); + if (getName().isEmpty()) { + cmd.setName(commandName); + } else { + cmd.setName(getName() + " " + commandName); } + cmd.setArguments(args.toArray(new String[args.size()])); provideStateTo(cmd); // atomicCmd.set(cmd); @@ -133,17 +138,17 @@ public class DispatchCommand extends BaseCommand { } } - private Command getCommand() throws UnloggedFailure { - if (root != null && root.containsKey(commandName)) { - return root.get(commandName); + private BaseCommand getCommand() throws UnloggedFailure { + if (dispatchers != null && dispatchers.containsKey(commandName)) { + return dispatchers.get(commandName); } - final Class c = getMap().get(commandName); + final Class c = getMap().get(commandName); if (c == null) { String msg = (getName().isEmpty() ? "Gitblit" : getName()) + ": " + commandName + ": not found"; throw new UnloggedFailure(1, msg); } - Command cmd = null; + BaseCommand cmd = null; try { cmd = c.newInstance(); } catch (Exception e) { @@ -153,38 +158,68 @@ public class DispatchCommand extends BaseCommand { } @Override - protected String usage() { - final StringBuilder usage = new StringBuilder(); - usage.append("Available commands"); - if (!getName().isEmpty()) { - usage.append(" of "); - usage.append(getName()); - } - usage.append(" are:\n"); - usage.append("\n"); - + public String usage() { + Set commands = new TreeSet(); + Set dispatchers = new TreeSet(); int maxLength = -1; - Map> m = getMap(); + Map> m = getMap(); for (String name : m.keySet()) { - maxLength = Math.max(maxLength, name.length()); - } - String format = "%-" + maxLength + "s %s"; - for (String name : Sets.newTreeSet(m.keySet())) { - final Class c = m.get(name); + Class c = m.get(name); CommandMetaData meta = c.getAnnotation(CommandMetaData.class); if (meta != null) { if (meta.hidden()) { continue; } + } + + maxLength = Math.max(maxLength, name.length()); + if (DispatchCommand.class.isAssignableFrom(c)) { + dispatchers.add(name); + } else { + commands.add(name); + } + } + String format = "%-" + maxLength + "s %s"; + + final StringBuilder usage = new StringBuilder(); + if (!commands.isEmpty()) { + usage.append("Available commands"); + if (!getName().isEmpty()) { + usage.append(" of "); + usage.append(getName()); + } + usage.append(" are:\n"); + usage.append("\n"); + for (String name : commands) { + final Class c = m.get(name); + CommandMetaData meta = c.getAnnotation(CommandMetaData.class); usage.append(" "); usage.append(String.format(format, name, Strings.nullToEmpty(meta.description()))); + usage.append("\n"); + } + usage.append("\n"); + } + + if (!dispatchers.isEmpty()) { + usage.append("Available command dispatchers"); + if (!getName().isEmpty()) { + usage.append(" of "); + usage.append(getName()); + } + usage.append(" are:\n"); + usage.append("\n"); + for (String name : dispatchers) { + final Class c = m.get(name); + CommandMetaData meta = c.getAnnotation(CommandMetaData.class); + usage.append(" "); + usage.append(String.format(format, name, Strings.nullToEmpty(meta.description()))); + usage.append("\n"); } usage.append("\n"); } - usage.append("\n"); usage.append("See '"); - if (getName().indexOf(' ') < 0) { + if (!StringUtils.isEmpty(getName())) { usage.append(getName()); usage.append(' '); } @@ -193,9 +228,9 @@ public class DispatchCommand extends BaseCommand { return usage.toString(); } - protected void provideStateTo(final Command cmd) { + protected void provideStateTo(final BaseCommand cmd) { if (cmd instanceof BaseCommand) { - ((BaseCommand) cmd).setContext(ctx); + cmd.setContext(ctx); } cmd.setInputStream(in); cmd.setOutputStream(out); -- cgit v1.2.3 From 9d44ade215922fd9e7bda7c604b31c31d217d750 Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 17 Mar 2014 21:23:44 -0400 Subject: Create git and gitblit dispatchers --- .../gitblit/transport/ssh/SshCommandFactory.java | 41 ++----- .../transport/ssh/commands/AddKeyCommand.java | 53 --------- .../transport/ssh/commands/BaseGitCommand.java | 103 ----------------- .../transport/ssh/commands/BaseKeyCommand.java | 58 ---------- .../transport/ssh/commands/CreateRepository.java | 36 ------ .../transport/ssh/commands/DispatchCommand.java | 65 +++++------ .../ssh/commands/ListRepositoriesCommand.java | 72 ------------ .../gitblit/transport/ssh/commands/Receive.java | 33 ------ .../transport/ssh/commands/RemoveKeyCommand.java | 61 ----------- .../transport/ssh/commands/ReviewCommand.java | 88 --------------- .../transport/ssh/commands/SetAccountCommand.java | 88 --------------- .../com/gitblit/transport/ssh/commands/Upload.java | 33 ------ .../transport/ssh/commands/VersionCommand.java | 29 ----- .../gitblit/transport/ssh/git/BaseGitCommand.java | 104 ++++++++++++++++++ .../transport/ssh/git/GitDispatchCommand.java | 61 +++++++++++ .../com/gitblit/transport/ssh/git/Receive.java | 33 ++++++ .../java/com/gitblit/transport/ssh/git/Upload.java | 33 ++++++ .../transport/ssh/gitblit/AddKeyCommand.java | 51 +++++++++ .../transport/ssh/gitblit/BaseKeyCommand.java | 64 +++++++++++ .../transport/ssh/gitblit/CreateRepository.java | 37 +++++++ .../ssh/gitblit/GitblitDispatchCommand.java | 38 +++++++ .../gitblit/transport/ssh/gitblit/LsCommand.java | 122 +++++++++++++++++++++ .../transport/ssh/gitblit/RemoveKeyCommand.java | 59 ++++++++++ .../transport/ssh/gitblit/ReviewCommand.java | 89 +++++++++++++++ .../transport/ssh/gitblit/SetAccountCommand.java | 85 ++++++++++++++ .../transport/ssh/gitblit/VersionCommand.java | 30 +++++ 26 files changed, 840 insertions(+), 726 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/Receive.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/Upload.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/git/Receive.java create mode 100644 src/main/java/com/gitblit/transport/ssh/git/Upload.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index b8dd5b9f..de7aad1f 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -34,21 +34,11 @@ import org.apache.sshd.server.session.ServerSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.manager.IGitblit; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.AddKeyCommand; -import com.gitblit.transport.ssh.commands.CreateRepository; import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.ListRepositoriesCommand; -import com.gitblit.transport.ssh.commands.Receive; -import com.gitblit.transport.ssh.commands.RemoveKeyCommand; -import com.gitblit.transport.ssh.commands.ReviewCommand; -import com.gitblit.transport.ssh.commands.SetAccountCommand; -import com.gitblit.transport.ssh.commands.Upload; -import com.gitblit.transport.ssh.commands.VersionCommand; +import com.gitblit.transport.ssh.git.GitDispatchCommand; +import com.gitblit.transport.ssh.gitblit.GitblitDispatchCommand; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.WorkQueue; import com.google.common.util.concurrent.Atomics; @@ -86,31 +76,16 @@ public class SshCommandFactory implements CommandFactory { protected DispatchCommand createRootDispatcher(SshDaemonClient client, String cmdLine) { final UserModel user = client.getUser(); - DispatchCommand gitblitCmd = new DispatchCommand(); - gitblitCmd.registerCommand(user, VersionCommand.class); - gitblitCmd.registerCommand(user, AddKeyCommand.class); - gitblitCmd.registerCommand(user, RemoveKeyCommand.class); - gitblitCmd.registerCommand(user, ListRepositoriesCommand.class); - gitblitCmd.registerCommand(user, ReviewCommand.class); - - gitblitCmd.registerCommand(user, CreateRepository.class); - gitblitCmd.registerCommand(user, SetAccountCommand.class); - - DispatchCommand gitCmd = new DispatchCommand(); - gitCmd.registerCommand(user, Upload.class); - gitCmd.registerCommand(user, Receive.class); + DispatchCommand root = new DispatchCommand() { + }; + root.setContext(new SshCommandContext(gitblit, client, cmdLine)); - DispatchCommand root = new DispatchCommand(); - root.registerDispatcher("gitblit", gitblitCmd); - root.registerDispatcher("git", gitCmd); + // TODO convert these dispatchers to plugin extension points + root.registerDispatcher(user, GitblitDispatchCommand.class); + root.registerDispatcher(user, GitDispatchCommand.class); - root.setRepositoryResolver(new RepositoryResolver(gitblit)); - root.setUploadPackFactory(new GitblitUploadPackFactory(gitblit)); - root.setReceivePackFactory(new GitblitReceivePackFactory(gitblit)); root.setAuthenticator(keyAuthenticator); - root.setContext(new SshCommandContext(gitblit, client, cmdLine)); - return root; } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java deleted file mode 100644 index 35bb1bbf..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/AddKeyCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.kohsuke.args4j.Argument; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.IKeyManager; - -/** - * Add a key to the current user's authorized keys list. - * - * @author James Moger - * - */ -@CommandMetaData(name = "add-key", description = "Add an SSH public key to your account") -public class AddKeyCommand extends BaseKeyCommand { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - @Argument(metaVar = "|KEY", usage = "the key to add") - private List addKeys = new ArrayList(); - - @Override - public void run() throws IOException, UnloggedFailure { - String username = ctx.getClient().getUsername(); - List keys = readKeys(addKeys); - IKeyManager keyManager = authenticator.getKeyManager(); - for (String key : keys) { - keyManager.addKey(username, key); - log.info("added SSH public key for {}", username); - } - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java deleted file mode 100644 index a3411616..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseGitCommand.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.io.IOException; - -import org.apache.sshd.server.Environment; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.resolver.ReceivePackFactory; -import org.eclipse.jgit.transport.resolver.UploadPackFactory; -import org.kohsuke.args4j.Argument; - -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; -import com.gitblit.transport.ssh.SshDaemonClient; - -/** - * @author Eric Myhre - * - */ -public abstract class BaseGitCommand extends BaseCommand { - @Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name") - protected String repository; - - protected RepositoryResolver repositoryResolver; - protected ReceivePackFactory receivePackFactory; - protected UploadPackFactory uploadPackFactory; - - protected Repository repo; - - @Override - public void start(final Environment env) { - startThread(new RepositoryCommandRunnable() { - @Override - public void run() throws Exception { - parseCommandLine(); - BaseGitCommand.this.service(); - } - - @Override - public String getRepository() { - return repository; - } - }); - } - - private void service() throws IOException, Failure { - try { - repo = openRepository(); - runImpl(); - } finally { - if (repo != null) { - repo.close(); - } - } - } - - protected abstract void runImpl() throws IOException, Failure; - - protected Repository openRepository() throws Failure { - // Assume any attempt to use \ was by a Windows client - // and correct to the more typical / used in Git URIs. - // - repository = repository.replace('\\', '/'); - // ssh://git@thishost/path should always be name="/path" here - // - if (!repository.startsWith("/")) { - throw new Failure(1, "fatal: '" + repository + "': not starts with / character"); - } - repository = repository.substring(1); - try { - return repositoryResolver.open(ctx.getClient(), repository); - } catch (Exception e) { - throw new Failure(1, "fatal: '" + repository + "': not a git archive", e); - } - } - - public void setRepositoryResolver(RepositoryResolver repositoryResolver) { - this.repositoryResolver = repositoryResolver; - } - - public void setReceivePackFactory(GitblitReceivePackFactory receivePackFactory) { - this.receivePackFactory = receivePackFactory; - } - - public void setUploadPackFactory(GitblitUploadPackFactory uploadPackFactory) { - this.uploadPackFactory = uploadPackFactory; - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java deleted file mode 100644 index f92ea6f9..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseKeyCommand.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.util.List; - -import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; -import com.google.common.base.Charsets; - -/** - * - * Base class for commands that read SSH keys from stdin or a parameter list. - * - */ -public abstract class BaseKeyCommand extends SshCommand { - - protected List readKeys(List sshKeys) - throws UnsupportedEncodingException, IOException { - int idx = -1; - if (sshKeys.isEmpty() || ((idx = sshKeys.indexOf("-")) >= 0)) { - String sshKey = ""; - BufferedReader br = new BufferedReader(new InputStreamReader( - in, Charsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - sshKey += line + "\n"; - } - if (idx == -1) { - sshKeys.add(sshKey.trim()); - } else { - sshKeys.set(idx, sshKey.trim()); - } - } - return sshKeys; - } - - protected CachingPublicKeyAuthenticator authenticator; - public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { - this.authenticator = authenticator; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java deleted file mode 100644 index 20f69015..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/CreateRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import org.kohsuke.args4j.Option; - -import com.gitblit.transport.ssh.CommandMetaData; - -@CommandMetaData(name = "create-repository", description = "Create new GIT repository", admin = true, hidden = true) -public class CreateRepository extends SshCommand { - - @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created") - private String name; - - @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of repository") - private String repositoryDescription; - - @Override - public void run() { - stdout.println(String.format("Repository <%s> was created", name)); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 00d3b819..38f1a48f 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -30,21 +30,17 @@ import org.kohsuke.args4j.Argument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; -import com.gitblit.transport.ssh.SshDaemonClient; -mport com.gitblit.utils.StringUtils; +import com.gitblit.transport.ssh.gitblit.BaseKeyCommand; +import com.gitblit.utils.StringUtils; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -public class DispatchCommand extends BaseCommand { +public abstract class DispatchCommand extends BaseCommand { private Logger log = LoggerFactory.getLogger(getClass()); @@ -62,11 +58,31 @@ public class DispatchCommand extends BaseCommand { commands = new HashSet>(); } - public void registerDispatcher(String name, Command cmd) { + public void registerDispatcher(UserModel user, Class cmd) { + if (!cmd.isAnnotationPresent(CommandMetaData.class)) { + throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(), + CommandMetaData.class.getName())); + } if (dispatchers == null) { dispatchers = Maps.newHashMap(); } - dispatchers.put(name, cmd); + + CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class); + if (meta.admin() && !user.canAdmin()) { + log.debug(MessageFormat.format("excluding admin dispatch command {0} for {1}", meta.name(), user.username)); + return; + } + + try { + DispatchCommand dispatcher = cmd.newInstance(); + dispatcher.registerCommands(user); + dispatchers.put(meta.name(), dispatcher); + } catch (Exception e) { + log.error("failed to register {} dispatcher", meta.name()); + } + } + + protected void registerCommands(UserModel user) { } @@ -237,41 +253,12 @@ public class DispatchCommand extends BaseCommand { cmd.setErrorStream(err); cmd.setExitCallback(exit); - if (cmd instanceof BaseGitCommand) { - BaseGitCommand a = (BaseGitCommand) cmd; - a.setRepositoryResolver(repositoryResolver); - a.setUploadPackFactory(gitblitUploadPackFactory); - a.setReceivePackFactory(gitblitReceivePackFactory); - } else if (cmd instanceof DispatchCommand) { - DispatchCommand d = (DispatchCommand) cmd; - d.setRepositoryResolver(repositoryResolver); - d.setUploadPackFactory(gitblitUploadPackFactory); - d.setReceivePackFactory(gitblitReceivePackFactory); - d.setAuthenticator(authenticator); - } else if (cmd instanceof BaseKeyCommand) { + if (cmd instanceof BaseKeyCommand) { BaseKeyCommand k = (BaseKeyCommand) cmd; k.setAuthenticator(authenticator); } } - private RepositoryResolver repositoryResolver; - - public void setRepositoryResolver(RepositoryResolver repositoryResolver) { - this.repositoryResolver = repositoryResolver; - } - - private GitblitUploadPackFactory gitblitUploadPackFactory; - - public void setUploadPackFactory(GitblitUploadPackFactory gitblitUploadPackFactory) { - this.gitblitUploadPackFactory = gitblitUploadPackFactory; - } - - private GitblitReceivePackFactory gitblitReceivePackFactory; - - public void setReceivePackFactory(GitblitReceivePackFactory gitblitReceivePackFactory) { - this.gitblitReceivePackFactory = gitblitReceivePackFactory; - } - private CachingPublicKeyAuthenticator authenticator; public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java deleted file mode 100644 index 7c58e7fc..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/ListRepositoriesCommand.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.text.MessageFormat; -import java.text.SimpleDateFormat; -import java.util.List; - -import org.kohsuke.args4j.Option; -import org.parboiled.common.StringUtils; - -import com.gitblit.manager.IGitblit; -import com.gitblit.models.RepositoryModel; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; - -@CommandMetaData(name = "repositories", description = "List the available repositories") -public class ListRepositoriesCommand extends SshCommand { - - @Option(name = "--verbose", aliases = { "-v" }, usage = "verbose") - private boolean verbose; - - @Override - public void run() { - IGitblit gitblit = ctx.getGitblit(); - UserModel user = ctx.getClient().getUser(); - List repositories = gitblit.getRepositoryModels(user); - int nameLen = 0; - int descLen = 0; - for (RepositoryModel repo : repositories) { - int len = repo.name.length(); - if (len > nameLen) { - nameLen = len; - } - if (!StringUtils.isEmpty(repo.description)) { - len = repo.description.length(); - if (len > descLen) { - descLen = len; - } - } - } - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); - - String pattern; - if (verbose) { - pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen); - } else { - pattern = "%s"; - } - - for (RepositoryModel repo : repositories) { - stdout.println(String.format(pattern, - repo.name, - repo.description == null ? "" : repo.description, - df.format(repo.lastChange))); - } - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java b/src/main/java/com/gitblit/transport/ssh/commands/Receive.java deleted file mode 100644 index 559cfa5d..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/Receive.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import org.eclipse.jgit.transport.ReceivePack; - -import com.gitblit.transport.ssh.CommandMetaData; - -@CommandMetaData(name = "git-receive-pack", description = "Receive pack") -public class Receive extends BaseGitCommand { - @Override - protected void runImpl() throws Failure { - try { - ReceivePack rp = receivePackFactory.create(ctx.getClient(), repo); - rp.receive(in, out, null); - } catch (Exception e) { - throw new Failure(1, "fatal: Cannot receive pack: ", e); - } - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java deleted file mode 100644 index 90e70418..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/RemoveKeyCommand.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.kohsuke.args4j.Argument; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.IKeyManager; - - -/** - * Remove an SSH public key from the current user's authorized key list. - * - * @author James Moger - * - */ -@CommandMetaData(name = "rm-key", description = "Remove an SSH public key from your account") -public class RemoveKeyCommand extends BaseKeyCommand { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - private static final String ALL = "ALL"; - - @Argument(metaVar = "||ALL", usage = "the key to remove") - private List removeKeys = new ArrayList(); - - @Override - public void run() throws IOException, UnloggedFailure { - String username = ctx.getClient().getUsername(); - List keys = readKeys(removeKeys); - IKeyManager keyManager = authenticator.getKeyManager(); - if (keys.contains(ALL)) { - keyManager.removeAllKeys(username); - log.info("removed all SSH public keys from {}", username); - } else { - for (String key : keys) { - keyManager.removeKey(username, key); - log.info("removed SSH public key from {}", username); - } - } - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java deleted file mode 100644 index b088a2e6..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/ReviewCommand.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import java.util.HashSet; -import java.util.Set; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import com.gitblit.models.TicketModel.Change; -import com.gitblit.models.TicketModel.Patchset; -import com.gitblit.models.TicketModel.Score; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.wicket.GitBlitWebSession; - -@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets", hidden = true) -public class ReviewCommand extends SshCommand { - - private final static short REV_ID_LEN = 40; - private final Set patchSets = new HashSet(); - - @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review") - void addPatchSetId(final String token) { - try { - patchSets.add(parsePatchSet(token)); - } catch (UnloggedFailure e) { - throw new IllegalArgumentException(e.getMessage(), e); - } - } - - @Option(name = "--project", required = true, aliases = "-p", usage = "project containing the specified patch set(s)") - private String project; - - @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change(s)", metaVar = "MESSAGE") - private String changeComment; - - @Option(name = "--vote", aliases = "-v", usage = "vote on this patch set", metaVar = "VOTE") - private int vote; - - @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)") - private boolean submitChange; - - @Override - public void run() throws UnloggedFailure { - UserModel user = GitBlitWebSession.get().getUser(); - // TODO ensure user has permission to score +2/-2 - for (Patchset ps : patchSets) { - // review - Change change = new Change(user.username); - change.review(ps, Score.fromScore(vote), false); - // TODO(davido): add patchset comment - if (submitChange) { - // TODO(davido): merge (when desired and the change is mergeable) - } - } - } - - private Patchset parsePatchSet(String ps) throws UnloggedFailure { - // By commit? - // - if (ps.matches("^([0-9a-fA-F]{4," + REV_ID_LEN + "})$")) { - // TODO; parse - } - - // By older style change,patchset? - // - if (ps.matches("^[1-9][0-9]*,[1-9][0-9]*$")) { - // TODO: parse - } - - throw new UnloggedFailure(1, "fatal: Cannot parse patchset: " + ps); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java deleted file mode 100644 index 1f0d902b..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/SetAccountCommand.java +++ /dev/null @@ -1,88 +0,0 @@ -//Copyright (C) 2012 The Android Open Source Project -// -//Licensed 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 com.gitblit.transport.ssh.commands; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.IKeyManager; - -/** Set a user's account settings. **/ -@CommandMetaData(name = "set-account", description = "Change an account's settings", admin = true) -public class SetAccountCommand extends BaseKeyCommand { - - private static final String ALL = "ALL"; - - @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id") - private String user; - - @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account") - private List addSshKeys = new ArrayList(); - - @Option(name = "--delete-ssh-key", metaVar = "-|KEY", usage = "public keys to delete from the account") - private List deleteSshKeys = new ArrayList(); - - @Override - public void run() throws IOException, UnloggedFailure { - validate(); - setAccount(); - } - - private void validate() throws UnloggedFailure { - if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) { - throw new UnloggedFailure(1, "Only one option may use the stdin"); - } - if (deleteSshKeys.contains(ALL)) { - deleteSshKeys = Collections.singletonList(ALL); - } - } - - private void setAccount() throws IOException, UnloggedFailure { - addSshKeys = readKeys(addSshKeys); - if (!addSshKeys.isEmpty()) { - addSshKeys(addSshKeys); - } - - deleteSshKeys = readKeys(deleteSshKeys); - if (!deleteSshKeys.isEmpty()) { - deleteSshKeys(deleteSshKeys); - } - } - - private void addSshKeys(List sshKeys) throws UnloggedFailure, - IOException { - IKeyManager keyManager = authenticator.getKeyManager(); - for (String sshKey : sshKeys) { - keyManager.addKey(user, sshKey); - } - } - - private void deleteSshKeys(List sshKeys) { - IKeyManager keyManager = authenticator.getKeyManager(); - if (sshKeys.contains(ALL)) { - keyManager.removeAllKeys(user); - } else { - for (String sshKey : sshKeys) { - keyManager.removeKey(user, sshKey); - } - } - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java b/src/main/java/com/gitblit/transport/ssh/commands/Upload.java deleted file mode 100644 index ac98bb20..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/Upload.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import org.eclipse.jgit.transport.UploadPack; - -import com.gitblit.transport.ssh.CommandMetaData; - -@CommandMetaData(name = "git-upload-pack", description = "Upload pack") -public class Upload extends BaseGitCommand { - @Override - protected void runImpl() throws Failure { - try { - UploadPack up = uploadPackFactory.create(ctx.getClient(), repo); - up.upload(in, out, null); - } catch (Exception e) { - throw new Failure(1, "fatal: Cannot upload pack: ", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java deleted file mode 100644 index c2c4f52d..00000000 --- a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.commands; - -import com.gitblit.Constants; -import com.gitblit.transport.ssh.CommandMetaData; - -@CommandMetaData(name="version", description = "Display the Gitblit version") -public class VersionCommand extends SshCommand { - - @Override - public void run() { - stdout.println(Constants.getGitBlitVersion()); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java b/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java new file mode 100644 index 00000000..b203d476 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java @@ -0,0 +1,104 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.git; + +import java.io.IOException; + +import org.apache.sshd.server.Environment; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.kohsuke.args4j.Argument; + +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.transport.ssh.SshDaemonClient; +import com.gitblit.transport.ssh.commands.BaseCommand; + +/** + * @author Eric Myhre + * + */ +abstract class BaseGitCommand extends BaseCommand { + @Argument(index = 0, metaVar = "REPOSITORY", required = true, usage = "repository name") + protected String repository; + + protected RepositoryResolver repositoryResolver; + protected ReceivePackFactory receivePackFactory; + protected UploadPackFactory uploadPackFactory; + + protected Repository repo; + + @Override + public void start(final Environment env) { + startThread(new RepositoryCommandRunnable() { + @Override + public void run() throws Exception { + parseCommandLine(); + BaseGitCommand.this.service(); + } + + @Override + public String getRepository() { + return repository; + } + }); + } + + private void service() throws IOException, Failure { + try { + repo = openRepository(); + runImpl(); + } finally { + if (repo != null) { + repo.close(); + } + } + } + + protected abstract void runImpl() throws IOException, Failure; + + protected Repository openRepository() throws Failure { + // Assume any attempt to use \ was by a Windows client + // and correct to the more typical / used in Git URIs. + // + repository = repository.replace('\\', '/'); + // ssh://git@thishost/path should always be name="/path" here + // + if (!repository.startsWith("/")) { + throw new Failure(1, "fatal: '" + repository + "': not starts with / character"); + } + repository = repository.substring(1); + try { + return repositoryResolver.open(ctx.getClient(), repository); + } catch (Exception e) { + throw new Failure(1, "fatal: '" + repository + "': not a git archive", e); + } + } + + public void setRepositoryResolver(RepositoryResolver repositoryResolver) { + this.repositoryResolver = repositoryResolver; + } + + public void setReceivePackFactory(GitblitReceivePackFactory receivePackFactory) { + this.receivePackFactory = receivePackFactory; + } + + public void setUploadPackFactory(GitblitUploadPackFactory uploadPackFactory) { + this.uploadPackFactory = uploadPackFactory; + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java new file mode 100644 index 00000000..adeace52 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.git; + +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.SshCommandContext; +import com.gitblit.transport.ssh.SshDaemonClient; +import com.gitblit.transport.ssh.commands.BaseCommand; +import com.gitblit.transport.ssh.commands.DispatchCommand; + +@CommandMetaData(name = "git", description="Dispatcher for git receive and upload commands", hidden = true) +public class GitDispatchCommand extends DispatchCommand { + + protected RepositoryResolver repositoryResolver; + protected GitblitUploadPackFactory uploadPackFactory; + protected GitblitReceivePackFactory receivePackFactory; + + @Override + public void setContext(SshCommandContext context) { + super.setContext(context); + + IGitblit gitblit = context.getGitblit(); + repositoryResolver = new RepositoryResolver(gitblit); + uploadPackFactory = new GitblitUploadPackFactory(gitblit); + receivePackFactory = new GitblitReceivePackFactory(gitblit); + } + + @Override + protected void registerCommands(UserModel user) { + registerCommand(user, Upload.class); + registerCommand(user, Receive.class); + } + + @Override + protected void provideStateTo(final BaseCommand cmd) { + super.provideStateTo(cmd); + + BaseGitCommand a = (BaseGitCommand) cmd; + a.setRepositoryResolver(repositoryResolver); + a.setUploadPackFactory(uploadPackFactory); + a.setReceivePackFactory(receivePackFactory); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/git/Receive.java b/src/main/java/com/gitblit/transport/ssh/git/Receive.java new file mode 100644 index 00000000..4089f1df --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/git/Receive.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.git; + +import org.eclipse.jgit.transport.ReceivePack; + +import com.gitblit.transport.ssh.CommandMetaData; + +@CommandMetaData(name = "git-receive-pack", description = "Receives pushes from a client") +public class Receive extends BaseGitCommand { + @Override + protected void runImpl() throws Failure { + try { + ReceivePack rp = receivePackFactory.create(ctx.getClient(), repo); + rp.receive(in, out, null); + } catch (Exception e) { + throw new Failure(1, "fatal: Cannot receive pack: ", e); + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/git/Upload.java b/src/main/java/com/gitblit/transport/ssh/git/Upload.java new file mode 100644 index 00000000..5793c3e6 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/git/Upload.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.git; + +import org.eclipse.jgit.transport.UploadPack; + +import com.gitblit.transport.ssh.CommandMetaData; + +@CommandMetaData(name = "git-upload-pack", description = "Sends packs to a client for clone and fetch") +public class Upload extends BaseGitCommand { + @Override + protected void runImpl() throws Failure { + try { + UploadPack up = uploadPackFactory.create(ctx.getClient(), repo); + up.upload(in, out, null); + } catch (Exception e) { + throw new Failure(1, "fatal: Cannot upload pack: ", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java new file mode 100644 index 00000000..6d5c85c7 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.transport.ssh.CommandMetaData; + +/** + * Add a key to the current user's authorized keys list. + * + * @author James Moger + * + */ +@CommandMetaData(name = "add-key", description = "Add an SSH public key to your account") +public class AddKeyCommand extends BaseKeyCommand { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + @Argument(metaVar = "|KEY", usage = "the key to add") + private List addKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + String username = ctx.getClient().getUsername(); + List keys = readKeys(addKeys); + for (String key : keys) { + getKeyManager().addKey(username, key); + log.info("added SSH public key for {}", username); + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java new file mode 100644 index 00000000..09099578 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.List; + +import com.gitblit.transport.ssh.IKeyManager; +import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; +import com.gitblit.transport.ssh.commands.SshCommand; +import com.google.common.base.Charsets; + +/** + * + * Base class for commands that read SSH keys from stdin or a parameter list. + * + */ +abstract class BaseKeyCommand extends SshCommand { + + protected List readKeys(List sshKeys) + throws UnsupportedEncodingException, IOException { + int idx = -1; + if (sshKeys.isEmpty() || ((idx = sshKeys.indexOf("-")) >= 0)) { + String sshKey = ""; + BufferedReader br = new BufferedReader(new InputStreamReader( + in, Charsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + sshKey += line + "\n"; + } + if (idx == -1) { + sshKeys.add(sshKey.trim()); + } else { + sshKeys.set(idx, sshKey.trim()); + } + } + return sshKeys; + } + + protected CachingPublicKeyAuthenticator authenticator; + public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { + this.authenticator = authenticator; + } + + protected IKeyManager getKeyManager() { + return authenticator.getKeyManager(); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java new file mode 100644 index 00000000..b2e1b1b0 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import org.kohsuke.args4j.Option; + +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.SshCommand; + +@CommandMetaData(name = "create-repository", description = "Create new GIT repository", admin = true, hidden = true) +public class CreateRepository extends SshCommand { + + @Option(name = "--name", aliases = {"-n"}, required = true, metaVar = "NAME", usage = "name of repository to be created") + private String name; + + @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of repository") + private String repositoryDescription; + + @Override + public void run() { + stdout.println(String.format("Repository <%s> was created", name)); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java new file mode 100644 index 00000000..544b204f --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.DispatchCommand; + +@CommandMetaData(name = "gitblit", description = "Gitblit server commands") +public class GitblitDispatchCommand extends DispatchCommand { + + @Override + protected void registerCommands(UserModel user) { + // normal usage commands + registerCommand(user, VersionCommand.class); + registerCommand(user, AddKeyCommand.class); + registerCommand(user, RemoveKeyCommand.class); + registerCommand(user, LsCommand.class); + registerCommand(user, ReviewCommand.class); + + // administrative commands + registerCommand(user, CreateRepository.class); + registerCommand(user, SetAccountCommand.class); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java new file mode 100644 index 00000000..cf50a2ee --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java @@ -0,0 +1,122 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.List; + +import org.kohsuke.args4j.Option; +import org.parboiled.common.StringUtils; + +import com.gitblit.manager.IGitblit; +import com.gitblit.models.ProjectModel; +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.SshCommand; + +@CommandMetaData(name = "ls", description = "List repositories or projects") +public class LsCommand extends SshCommand { + + @Option(name = "--projects", aliases = { "-p" }, usage = "list projects") + private boolean projects; + + @Option(name = "--verbose", aliases = { "-v" }, usage = "verbose") + private boolean verbose; + + @Override + public void run() { + if (projects) { + listProjects(); + } else { + listRepositories(); + } + } + + protected void listProjects() { + IGitblit gitblit = ctx.getGitblit(); + UserModel user = ctx.getClient().getUser(); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + + List projects = gitblit.getProjectModels(user, false); + int nameLen = 0; + int descLen = 0; + for (ProjectModel project : projects) { + int len = project.name.length(); + if (len > nameLen) { + nameLen = len; + } + if (!StringUtils.isEmpty(project.description)) { + len = project.description.length(); + if (len > descLen) { + descLen = len; + } + } + } + + String pattern; + if (verbose) { + pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen); + } else { + pattern = "%s"; + } + + for (ProjectModel project : projects) { + stdout.println(String.format(pattern, + project.name, + project.description == null ? "" : project.description, + df.format(project.lastChange))); + } + } + + protected void listRepositories() { + IGitblit gitblit = ctx.getGitblit(); + UserModel user = ctx.getClient().getUser(); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + + List repositories = gitblit.getRepositoryModels(user); + int nameLen = 0; + int descLen = 0; + for (RepositoryModel repo : repositories) { + int len = repo.name.length(); + if (len > nameLen) { + nameLen = len; + } + if (!StringUtils.isEmpty(repo.description)) { + len = repo.description.length(); + if (len > descLen) { + descLen = len; + } + } + } + + String pattern; + if (verbose) { + pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%s", nameLen, descLen); + } else { + pattern = "%s"; + } + + for (RepositoryModel repo : repositories) { + stdout.println(String.format(pattern, + repo.name, + repo.description == null ? "" : repo.description, + df.format(repo.lastChange))); + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java new file mode 100644 index 00000000..7c9abfd4 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.transport.ssh.CommandMetaData; + + +/** + * Remove an SSH public key from the current user's authorized key list. + * + * @author James Moger + * + */ +@CommandMetaData(name = "rm-key", description = "Remove an SSH public key from your account") +public class RemoveKeyCommand extends BaseKeyCommand { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private static final String ALL = "ALL"; + + @Argument(metaVar = "||ALL", usage = "the key to remove") + private List removeKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + String username = ctx.getClient().getUsername(); + List keys = readKeys(removeKeys); + if (keys.contains(ALL)) { + getKeyManager().removeAllKeys(username); + log.info("removed all SSH public keys from {}", username); + } else { + for (String key : keys) { + getKeyManager().removeKey(username, key); + log.info("removed SSH public key from {}", username); + } + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java new file mode 100644 index 00000000..9e4d8ba7 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java @@ -0,0 +1,89 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.util.HashSet; +import java.util.Set; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import com.gitblit.models.TicketModel.Change; +import com.gitblit.models.TicketModel.Patchset; +import com.gitblit.models.TicketModel.Score; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.SshCommand; +import com.gitblit.wicket.GitBlitWebSession; + +@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets", hidden = true) +public class ReviewCommand extends SshCommand { + + private final static short REV_ID_LEN = 40; + private final Set patchSets = new HashSet(); + + @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review") + void addPatchSetId(final String token) { + try { + patchSets.add(parsePatchSet(token)); + } catch (UnloggedFailure e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + @Option(name = "--project", required = true, aliases = "-p", usage = "project containing the specified patch set(s)") + private String project; + + @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change(s)", metaVar = "MESSAGE") + private String changeComment; + + @Option(name = "--vote", aliases = "-v", usage = "vote on this patch set", metaVar = "VOTE") + private int vote; + + @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)") + private boolean submitChange; + + @Override + public void run() throws UnloggedFailure { + UserModel user = GitBlitWebSession.get().getUser(); + // TODO ensure user has permission to score +2/-2 + for (Patchset ps : patchSets) { + // review + Change change = new Change(user.username); + change.review(ps, Score.fromScore(vote), false); + // TODO(davido): add patchset comment + if (submitChange) { + // TODO(davido): merge (when desired and the change is mergeable) + } + } + } + + private Patchset parsePatchSet(String ps) throws UnloggedFailure { + // By commit? + // + if (ps.matches("^([0-9a-fA-F]{4," + REV_ID_LEN + "})$")) { + // TODO; parse + } + + // By older style change,patchset? + // + if (ps.matches("^[1-9][0-9]*,[1-9][0-9]*$")) { + // TODO: parse + } + + throw new UnloggedFailure(1, "fatal: Cannot parse patchset: " + ps); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java new file mode 100644 index 00000000..28ac9e19 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java @@ -0,0 +1,85 @@ +//Copyright (C) 2012 The Android Open Source Project +// +//Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +import com.gitblit.transport.ssh.CommandMetaData; + +/** Set a user's account settings. **/ +@CommandMetaData(name = "set-account", description = "Change an account's settings", admin = true) +public class SetAccountCommand extends BaseKeyCommand { + + private static final String ALL = "ALL"; + + @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id") + private String user; + + @Option(name = "--add-ssh-key", metaVar = "-|KEY", usage = "public keys to add to the account") + private List addSshKeys = new ArrayList(); + + @Option(name = "--delete-ssh-key", metaVar = "-|KEY", usage = "public keys to delete from the account") + private List deleteSshKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + validate(); + setAccount(); + } + + private void validate() throws UnloggedFailure { + if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) { + throw new UnloggedFailure(1, "Only one option may use the stdin"); + } + if (deleteSshKeys.contains(ALL)) { + deleteSshKeys = Collections.singletonList(ALL); + } + } + + private void setAccount() throws IOException, UnloggedFailure { + addSshKeys = readKeys(addSshKeys); + if (!addSshKeys.isEmpty()) { + addSshKeys(addSshKeys); + } + + deleteSshKeys = readKeys(deleteSshKeys); + if (!deleteSshKeys.isEmpty()) { + deleteSshKeys(deleteSshKeys); + } + } + + private void addSshKeys(List sshKeys) throws UnloggedFailure, + IOException { + for (String sshKey : sshKeys) { + getKeyManager().addKey(user, sshKey); + } + } + + private void deleteSshKeys(List sshKeys) { + if (sshKeys.contains(ALL)) { + getKeyManager().removeAllKeys(user); + } else { + for (String sshKey : sshKeys) { + getKeyManager().removeKey(user, sshKey); + } + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java new file mode 100644 index 00000000..513f6d96 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java @@ -0,0 +1,30 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import com.gitblit.Constants; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.SshCommand; + +@CommandMetaData(name="version", description = "Display the Gitblit version") +public class VersionCommand extends SshCommand { + + @Override + public void run() { + stdout.println(Constants.getGitBlitVersion()); + } +} -- cgit v1.2.3 From 245836904ba5cecdc31773cf7c9616396c8ad8c0 Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 17 Mar 2014 21:30:46 -0400 Subject: Elevate the public key manager to a top-level manager --- src/main/distrib/data/gitblit.properties | 5 - src/main/java/com/gitblit/DaggerModule.java | 37 ++++++- src/main/java/com/gitblit/FederationClient.java | 2 +- src/main/java/com/gitblit/GitBlit.java | 5 +- .../java/com/gitblit/manager/GitblitManager.java | 10 ++ src/main/java/com/gitblit/manager/IGitblit.java | 8 ++ .../java/com/gitblit/servlet/GitblitContext.java | 4 +- .../ssh/CachingPublicKeyAuthenticator.java | 15 ++- .../com/gitblit/transport/ssh/FileKeyManager.java | 5 +- .../com/gitblit/transport/ssh/IKeyManager.java | 78 --------------- .../gitblit/transport/ssh/IPublicKeyManager.java | 82 ++++++++++++++++ .../gitblit/transport/ssh/MemoryKeyManager.java | 98 +++++++++++++++++++ .../com/gitblit/transport/ssh/NullKeyManager.java | 5 +- .../gitblit/transport/ssh/SshCommandFactory.java | 8 +- .../java/com/gitblit/transport/ssh/SshDaemon.java | 107 +-------------------- .../transport/ssh/commands/DispatchCommand.java | 13 --- .../transport/ssh/gitblit/BaseKeyCommand.java | 12 +-- .../java/com/gitblit/wicket/GitBlitWebApp.java | 9 ++ src/test/config/test-gitblit.properties | 2 +- .../gitblit/tests/BogusPublicKeyAuthenticator.java | 39 -------- src/test/java/com/gitblit/tests/GitBlitSuite.java | 18 +--- src/test/java/com/gitblit/tests/SshDaemonTest.java | 25 +++++ 22 files changed, 300 insertions(+), 287 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/IKeyManager.java create mode 100644 src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java create mode 100644 src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java delete mode 100644 src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 52bb252b..64a52f5c 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -129,11 +129,6 @@ git.sshKeysFolder= ${baseFolder}/ssh # SINCE 1.5.0 git.sshBackend = NIO2 -# SSH public key authenticator -# -# SINCE 1.5.0 -git.sshPublicKeyAuthenticator = com.gitblit.transport.ssh.CachingPublicKeyAuthenticator - # Allow push/pull over http/https with JGit servlet. # If you do NOT want to allow Git clients to clone/push to Gitblit set this # to false. You might want to do this if you are only using ssh:// or git://. diff --git a/src/main/java/com/gitblit/DaggerModule.java b/src/main/java/com/gitblit/DaggerModule.java index 5ae8b253..b109f1db 100644 --- a/src/main/java/com/gitblit/DaggerModule.java +++ b/src/main/java/com/gitblit/DaggerModule.java @@ -32,6 +32,11 @@ import com.gitblit.manager.ProjectManager; import com.gitblit.manager.RepositoryManager; import com.gitblit.manager.RuntimeManager; import com.gitblit.manager.UserManager; +import com.gitblit.transport.ssh.FileKeyManager; +import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.transport.ssh.MemoryKeyManager; +import com.gitblit.transport.ssh.NullKeyManager; +import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebApp; import dagger.Module; @@ -53,6 +58,7 @@ import dagger.Provides; INotificationManager.class, IUserManager.class, IAuthenticationManager.class, + IPublicKeyManager.class, IRepositoryManager.class, IProjectManager.class, IFederationManager.class, @@ -62,7 +68,7 @@ import dagger.Provides; // the Gitblit Wicket app GitBlitWebApp.class - } + } ) public class DaggerModule { @@ -91,6 +97,31 @@ public class DaggerModule { userManager); } + @Provides @Singleton IPublicKeyManager providePublicKeyManager( + IStoredSettings settings, + IRuntimeManager runtimeManager) { + + String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName()); + if (StringUtils.isEmpty(clazz)) { + clazz = FileKeyManager.class.getName(); + } + if (FileKeyManager.class.getName().equals(clazz)) { + return new FileKeyManager(runtimeManager); + } else if (NullKeyManager.class.getName().equals(clazz)) { + return new NullKeyManager(); + } else if (MemoryKeyManager.class.getName().equals(clazz)) { + return new MemoryKeyManager(); + } else { + try { + Class mgrClass = Class.forName(clazz); + return (IPublicKeyManager) mgrClass.newInstance(); + } catch (Exception e) { + + } + return null; + } + } + @Provides @Singleton IRepositoryManager provideRepositoryManager( IRuntimeManager runtimeManager, IUserManager userManager) { @@ -127,6 +158,7 @@ public class DaggerModule { INotificationManager notificationManager, IUserManager userManager, IAuthenticationManager authenticationManager, + IPublicKeyManager publicKeyManager, IRepositoryManager repositoryManager, IProjectManager projectManager, IFederationManager federationManager) { @@ -136,6 +168,7 @@ public class DaggerModule { notificationManager, userManager, authenticationManager, + publicKeyManager, repositoryManager, projectManager, federationManager); @@ -146,6 +179,7 @@ public class DaggerModule { INotificationManager notificationManager, IUserManager userManager, IAuthenticationManager authenticationManager, + IPublicKeyManager publicKeyManager, IRepositoryManager repositoryManager, IProjectManager projectManager, IFederationManager federationManager, @@ -156,6 +190,7 @@ public class DaggerModule { notificationManager, userManager, authenticationManager, + publicKeyManager, repositoryManager, projectManager, federationManager, diff --git a/src/main/java/com/gitblit/FederationClient.java b/src/main/java/com/gitblit/FederationClient.java index 792a6382..d20025f0 100644 --- a/src/main/java/com/gitblit/FederationClient.java +++ b/src/main/java/com/gitblit/FederationClient.java @@ -97,7 +97,7 @@ public class FederationClient { UserManager users = new UserManager(runtime).start(); RepositoryManager repositories = new RepositoryManager(runtime, users).start(); FederationManager federation = new FederationManager(runtime, notifications, repositories).start(); - IGitblit gitblit = new GitblitManager(runtime, notifications, users, null, repositories, null, federation); + IGitblit gitblit = new GitblitManager(runtime, notifications, users, null, null, repositories, null, federation); FederationPullService puller = new FederationPullService(gitblit, federation.getFederationRegistrations()) { @Override diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index 817d18cb..b223d03c 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -41,6 +41,7 @@ import com.gitblit.tickets.FileTicketService; import com.gitblit.tickets.ITicketService; import com.gitblit.tickets.NullTicketService; import com.gitblit.tickets.RedisTicketService; +import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.utils.StringUtils; import dagger.Module; @@ -67,6 +68,7 @@ public class GitBlit extends GitblitManager { INotificationManager notificationManager, IUserManager userManager, IAuthenticationManager authenticationManager, + IPublicKeyManager publicKeyManager, IRepositoryManager repositoryManager, IProjectManager projectManager, IFederationManager federationManager) { @@ -75,6 +77,7 @@ public class GitBlit extends GitblitManager { notificationManager, userManager, authenticationManager, + publicKeyManager, repositoryManager, projectManager, federationManager); @@ -262,7 +265,7 @@ public class GitBlit extends GitblitManager { FileTicketService.class, BranchTicketService.class, RedisTicketService.class - } + } ) class GitBlitModule { diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index 97e8efc9..8856715a 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -69,6 +69,7 @@ import com.gitblit.models.SettingModel; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.tickets.ITicketService; +import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.HttpUtils; import com.gitblit.utils.JsonUtils; @@ -107,6 +108,8 @@ public class GitblitManager implements IGitblit { protected final IAuthenticationManager authenticationManager; + protected final IPublicKeyManager publicKeyManager; + protected final IRepositoryManager repositoryManager; protected final IProjectManager projectManager; @@ -118,6 +121,7 @@ public class GitblitManager implements IGitblit { INotificationManager notificationManager, IUserManager userManager, IAuthenticationManager authenticationManager, + IPublicKeyManager publicKeyManager, IRepositoryManager repositoryManager, IProjectManager projectManager, IFederationManager federationManager) { @@ -127,6 +131,7 @@ public class GitblitManager implements IGitblit { this.notificationManager = notificationManager; this.userManager = userManager; this.authenticationManager = authenticationManager; + this.publicKeyManager = publicKeyManager; this.repositoryManager = repositoryManager; this.projectManager = projectManager; this.federationManager = federationManager; @@ -524,6 +529,11 @@ public class GitblitManager implements IGitblit { throw new RuntimeException("This class does not have a ticket service!"); } + @Override + public IPublicKeyManager getPublicKeyManager() { + return publicKeyManager; + } + /* * ISTOREDSETTINGS * diff --git a/src/main/java/com/gitblit/manager/IGitblit.java b/src/main/java/com/gitblit/manager/IGitblit.java index 50210e9d..f4221cf9 100644 --- a/src/main/java/com/gitblit/manager/IGitblit.java +++ b/src/main/java/com/gitblit/manager/IGitblit.java @@ -27,6 +27,7 @@ import com.gitblit.models.RepositoryUrl; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.tickets.ITicketService; +import com.gitblit.transport.ssh.IPublicKeyManager; public interface IGitblit extends IManager, IRuntimeManager, @@ -109,4 +110,11 @@ public interface IGitblit extends IManager, */ ITicketService getTicketService(); + /** + * Returns the SSH public key manager. + * + * @return the SSH public key manager + */ + IPublicKeyManager getPublicKeyManager(); + } \ No newline at end of file diff --git a/src/main/java/com/gitblit/servlet/GitblitContext.java b/src/main/java/com/gitblit/servlet/GitblitContext.java index d4ec9671..cf8bba01 100644 --- a/src/main/java/com/gitblit/servlet/GitblitContext.java +++ b/src/main/java/com/gitblit/servlet/GitblitContext.java @@ -47,6 +47,7 @@ import com.gitblit.manager.IProjectManager; import com.gitblit.manager.IRepositoryManager; import com.gitblit.manager.IRuntimeManager; import com.gitblit.manager.IUserManager; +import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.utils.ContainerUtils; import com.gitblit.utils.StringUtils; @@ -149,7 +150,7 @@ public class GitblitContext extends DaggerContext { String contextRealPath = context.getRealPath("/"); File contextFolder = (contextRealPath != null) ? new File(contextRealPath) : null; - // if the base folder dosen't match the default assume they don't want to use express, + // if the base folder dosen't match the default assume they don't want to use express, // this allows for other containers to customise the basefolder per context. String defaultBase = Constants.contextFolder$ + "/WEB-INF/data"; String base = lookupBaseFolderFromJndi(); @@ -178,6 +179,7 @@ public class GitblitContext extends DaggerContext { startManager(injector, INotificationManager.class); startManager(injector, IUserManager.class); startManager(injector, IAuthenticationManager.class); + startManager(injector, IPublicKeyManager.class); startManager(injector, IRepositoryManager.class); startManager(injector, IProjectManager.class); startManager(injector, IFederationManager.class); diff --git a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java index 7d6066c7..0120fa65 100644 --- a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java @@ -34,23 +34,23 @@ import com.gitblit.models.UserModel; import com.google.common.base.Preconditions; /** - * + * * @author Eric Myrhe - * + * */ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, SessionListener { protected final Logger log = LoggerFactory.getLogger(getClass()); - protected final IKeyManager keyManager; + protected final IPublicKeyManager keyManager; protected final IAuthenticationManager authManager; private final Map> cache = new ConcurrentHashMap>(); - public CachingPublicKeyAuthenticator(IKeyManager keyManager, + public CachingPublicKeyAuthenticator(IPublicKeyManager keyManager, IAuthenticationManager authManager) { this.keyManager = keyManager; this.authManager = authManager; @@ -101,16 +101,15 @@ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, return false; } - public IKeyManager getKeyManager() { - return keyManager; - } - + @Override public void sessionCreated(Session session) { } + @Override public void sessionEvent(Session sesssion, Event event) { } + @Override public void sessionClosed(Session session) { cache.remove(session); } diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index ae0bc9cf..defb4a3e 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -35,12 +35,12 @@ import com.google.common.base.Joiner; import com.google.common.io.Files; /** - * Manages SSH keys on the filesystem. + * Manages public keys on the filesystem. * * @author James Moger * */ -public class FileKeyManager extends IKeyManager { +public class FileKeyManager extends IPublicKeyManager { protected final IRuntimeManager runtimeManager; @@ -59,6 +59,7 @@ public class FileKeyManager extends IKeyManager { @Override public FileKeyManager start() { + log.info(toString()); return this; } diff --git a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IKeyManager.java deleted file mode 100644 index 12fce3df..00000000 --- a/src/main/java/com/gitblit/transport/ssh/IKeyManager.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.security.PublicKey; -import java.text.MessageFormat; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; - -/** - * - * @author James Moger - * - */ -public abstract class IKeyManager { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - protected final LoadingCache> keyCache = CacheBuilder - .newBuilder(). - expireAfterAccess(15, TimeUnit.MINUTES). - maximumSize(100) - .build(new CacheLoader>() { - @Override - public List load(String username) { - return getKeysImpl(username); - } - }); - - public abstract IKeyManager start(); - - public abstract boolean isReady(); - - public abstract IKeyManager stop(); - - public final List getKeys(String username) { - try { - if (isStale(username)) { - keyCache.invalidate(username); - } - return keyCache.get(username); - } catch (ExecutionException e) { - log.error(MessageFormat.format("failed to retrieve keys for {0}", username), e); - } - return null; - } - - protected abstract boolean isStale(String username); - - protected abstract List getKeysImpl(String username); - - public abstract boolean addKey(String username, String data); - - public abstract boolean removeKey(String username, String data); - - public abstract boolean removeAllKeys(String username); -} diff --git a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java new file mode 100644 index 00000000..5857a599 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; +import java.text.MessageFormat; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.manager.IManager; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +/** + * Parent class for public key managers. + * + * @author James Moger + * + */ +public abstract class IPublicKeyManager implements IManager { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + protected final LoadingCache> keyCache = CacheBuilder + .newBuilder(). + expireAfterAccess(15, TimeUnit.MINUTES). + maximumSize(100) + .build(new CacheLoader>() { + @Override + public List load(String username) { + return getKeysImpl(username); + } + }); + + @Override + public abstract IPublicKeyManager start(); + + public abstract boolean isReady(); + + @Override + public abstract IPublicKeyManager stop(); + + public final List getKeys(String username) { + try { + if (isStale(username)) { + keyCache.invalidate(username); + } + return keyCache.get(username); + } catch (ExecutionException e) { + log.error(MessageFormat.format("failed to retrieve keys for {0}", username), e); + } + return null; + } + + protected abstract boolean isStale(String username); + + protected abstract List getKeysImpl(String username); + + public abstract boolean addKey(String username, String data); + + public abstract boolean removeKey(String username, String data); + + public abstract boolean removeAllKeys(String username); +} diff --git a/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java new file mode 100644 index 00000000..26bd021a --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java @@ -0,0 +1,98 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh; + +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Memory public key manager. + * + * @author James Moger + * + */ +public class MemoryKeyManager extends IPublicKeyManager { + + Map> keys; + + public MemoryKeyManager() { + keys = new HashMap>(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + @Override + public MemoryKeyManager start() { + log.info(toString()); + return this; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public MemoryKeyManager stop() { + return this; + } + + @Override + protected boolean isStale(String username) { + return false; + } + + @Override + protected List getKeysImpl(String username) { + String id = username.toLowerCase(); + if (keys.containsKey(id)) { + return keys.get(id); + } + return null; + } + + @Override + public boolean addKey(String username, String data) { + return false; + } + + @Override + public boolean removeKey(String username, String data) { + return false; + } + + @Override + public boolean removeAllKeys(String username) { + String id = username.toLowerCase(); + keys.remove(id.toLowerCase()); + return true; + } + + /* Test method for populating the memory key manager */ + public void addKey(String username, PublicKey key) { + String id = username.toLowerCase(); + if (!keys.containsKey(id)) { + keys.put(id, new ArrayList()); + } + keys.get(id).add(key); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java index c76728d8..25860d6c 100644 --- a/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java @@ -19,12 +19,12 @@ import java.security.PublicKey; import java.util.List; /** - * Rejects all SSH key management requests. + * Rejects all public key management requests. * * @author James Moger * */ -public class NullKeyManager extends IKeyManager { +public class NullKeyManager extends IPublicKeyManager { public NullKeyManager() { } @@ -36,6 +36,7 @@ public class NullKeyManager extends IKeyManager { @Override public NullKeyManager start() { + log.info(toString()); return this; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java index de7aad1f..2b2093ea 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java @@ -52,14 +52,10 @@ public class SshCommandFactory implements CommandFactory { private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class); private final IGitblit gitblit; - private final CachingPublicKeyAuthenticator keyAuthenticator; private final ScheduledExecutorService startExecutor; - public SshCommandFactory(IGitblit gitblit, - CachingPublicKeyAuthenticator keyAuthenticator, - IdGenerator idGenerator) { + public SshCommandFactory(IGitblit gitblit, IdGenerator idGenerator) { this.gitblit = gitblit; - this.keyAuthenticator = keyAuthenticator; int threads = 2;// cfg.getInt("sshd","commandStartThreads", 2); WorkQueue workQueue = new WorkQueue(idGenerator); @@ -84,8 +80,6 @@ public class SshCommandFactory implements CommandFactory { root.registerDispatcher(user, GitblitDispatchCommand.class); root.registerDispatcher(user, GitDispatchCommand.class); - root.setAuthenticator(keyAuthenticator); - return root; } diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index b6c5d680..da9a3726 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -21,8 +21,6 @@ import java.net.InetSocketAddress; import java.text.MessageFormat; import java.util.concurrent.atomic.AtomicBoolean; -import javax.inject.Singleton; - import org.apache.sshd.SshServer; import org.apache.sshd.common.io.IoServiceFactoryFactory; import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory; @@ -35,15 +33,10 @@ import org.slf4j.LoggerFactory; import com.gitblit.Constants; import com.gitblit.IStoredSettings; import com.gitblit.Keys; -import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IGitblit; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; -import dagger.Module; -import dagger.ObjectGraph; -import dagger.Provides; - /** * Manager for the ssh transport. Roughly analogous to the * {@link com.gitblit.transport.git.GitDaemon} class. @@ -73,7 +66,6 @@ public class SshDaemon { private final IGitblit gitblit; private final SshServer sshd; - private final ObjectGraph injector; /** * Construct the Gitblit SSH daemon. @@ -82,15 +74,12 @@ public class SshDaemon { */ public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) { this.gitblit = gitblit; - this.injector = ObjectGraph.create(new SshModule()); IStoredSettings settings = gitblit.getSettings(); int port = settings.getInteger(Keys.git.sshPort, 0); String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost"); - IKeyManager keyManager = getKeyManager(); - String sshBackendStr = settings.getString(Keys.git.sshBackend, SshSessionBackend.NIO2.name()); SshSessionBackend backend = SshSessionBackend.valueOf(sshBackendStr); @@ -108,7 +97,7 @@ public class SshDaemon { File hostKeyStore = new File(gitblit.getBaseFolder(), HOST_KEY_STORE); CachingPublicKeyAuthenticator keyAuthenticator = - getPublicKeyAuthenticator(keyManager, gitblit); + new CachingPublicKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit); sshd = SshServer.setUpDefaultServer(); sshd.setPort(addr.getPort()); @@ -119,7 +108,7 @@ public class SshDaemon { sshd.setSessionFactory(new SshServerSessionFactory()); sshd.setFileSystemFactory(new DisabledFilesystemFactory()); sshd.setTcpipForwardingFilter(new NonForwardingFilter()); - sshd.setCommandFactory(new SshCommandFactory(gitblit, keyAuthenticator, idGenerator)); + sshd.setCommandFactory(new SshCommandFactory(gitblit, idGenerator)); sshd.setShellFactory(new WelcomeShell(settings)); String version = Constants.getGitBlitVersion() + " (" + sshd.getVersion() + ")"; @@ -128,27 +117,6 @@ public class SshDaemon { run = new AtomicBoolean(false); } - private CachingPublicKeyAuthenticator getPublicKeyAuthenticator( - IKeyManager keyManager, IGitblit gitblit) { - IStoredSettings settings = gitblit.getSettings(); - String clazz = settings.getString(Keys.git.sshPublicKeyAuthenticator, - CachingPublicKeyAuthenticator.class.getName()); - if (StringUtils.isEmpty(clazz)) { - clazz = CachingPublicKeyAuthenticator.class.getName(); - } - try { - Class authClass = - (Class) Class.forName(clazz); - return authClass.getConstructor( - new Class[] { IKeyManager.class, - IAuthenticationManager.class }).newInstance( - keyManager, gitblit); - } catch (Exception e) { - log.error("failed to create ssh auth manager " + clazz, e); - } - return null; - } - public String formatUrl(String gituser, String servername, String repository) { if (sshd.getPort() == DEFAULT_PORT) { // standard port @@ -203,75 +171,4 @@ public class SshDaemon { } } } - - @SuppressWarnings("unchecked") - protected IKeyManager getKeyManager() { - IKeyManager keyManager = null; - IStoredSettings settings = gitblit.getSettings(); - String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName()); - if (StringUtils.isEmpty(clazz)) { - clazz = FileKeyManager.class.getName(); - } - try { - Class managerClass = (Class) Class.forName(clazz); - keyManager = injector.get(managerClass).start(); - if (keyManager.isReady()) { - log.info("{} is ready.", keyManager); - } else { - log.warn("{} is disabled.", keyManager); - } - } catch (Exception e) { - log.error("failed to create ssh key manager " + clazz, e); - keyManager = injector.get(NullKeyManager.class).start(); - } - return keyManager; - } - - @SuppressWarnings("unchecked") - protected IKeyManager getKeyAuthenticator() { - IKeyManager keyManager = null; - IStoredSettings settings = gitblit.getSettings(); - String clazz = settings.getString(Keys.git.sshKeysManager, FileKeyManager.class.getName()); - if (StringUtils.isEmpty(clazz)) { - clazz = FileKeyManager.class.getName(); - } - try { - Class managerClass = (Class) Class.forName(clazz); - keyManager = injector.get(managerClass).start(); - if (keyManager.isReady()) { - log.info("{} is ready.", keyManager); - } else { - log.warn("{} is disabled.", keyManager); - } - } catch (Exception e) { - log.error("failed to create ssh key manager " + clazz, e); - keyManager = injector.get(NullKeyManager.class).start(); - } - return keyManager; - } - - /** - * A nested Dagger graph is used for constructor dependency injection of - * complex classes. - * - * @author James Moger - * - */ - @Module( - library = true, - injects = { - NullKeyManager.class, - FileKeyManager.class - } - ) - class SshModule { - - @Provides @Singleton NullKeyManager provideNullKeyManager() { - return new NullKeyManager(); - } - - @Provides @Singleton FileKeyManager provideFileKeyManager() { - return new FileKeyManager(SshDaemon.this.gitblit); - } - } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index 38f1a48f..dd581f4d 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -32,8 +32,6 @@ import org.slf4j.LoggerFactory; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; -import com.gitblit.transport.ssh.gitblit.BaseKeyCommand; import com.gitblit.utils.StringUtils; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; @@ -252,16 +250,5 @@ public abstract class DispatchCommand extends BaseCommand { cmd.setOutputStream(out); cmd.setErrorStream(err); cmd.setExitCallback(exit); - - if (cmd instanceof BaseKeyCommand) { - BaseKeyCommand k = (BaseKeyCommand) cmd; - k.setAuthenticator(authenticator); - } - } - - private CachingPublicKeyAuthenticator authenticator; - - public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { - this.authenticator = authenticator; } } diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java index 09099578..1b7bac11 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java @@ -21,8 +21,7 @@ import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.util.List; -import com.gitblit.transport.ssh.IKeyManager; -import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; +import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.transport.ssh.commands.SshCommand; import com.google.common.base.Charsets; @@ -53,12 +52,7 @@ abstract class BaseKeyCommand extends SshCommand { return sshKeys; } - protected CachingPublicKeyAuthenticator authenticator; - public void setAuthenticator(CachingPublicKeyAuthenticator authenticator) { - this.authenticator = authenticator; - } - - protected IKeyManager getKeyManager() { - return authenticator.getKeyManager(); + protected IPublicKeyManager getKeyManager() { + return ctx.getGitblit().getPublicKeyManager(); } } diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java index 445335ff..6e8aa05f 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java @@ -39,6 +39,7 @@ import com.gitblit.manager.IRepositoryManager; import com.gitblit.manager.IRuntimeManager; import com.gitblit.manager.IUserManager; import com.gitblit.tickets.ITicketService; +import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.pages.ActivityPage; import com.gitblit.wicket.pages.BlamePage; @@ -95,6 +96,8 @@ public class GitBlitWebApp extends WebApplication { private final IAuthenticationManager authenticationManager; + private final IPublicKeyManager publicKeyManager; + private final IRepositoryManager repositoryManager; private final IProjectManager projectManager; @@ -108,6 +111,7 @@ public class GitBlitWebApp extends WebApplication { INotificationManager notificationManager, IUserManager userManager, IAuthenticationManager authenticationManager, + IPublicKeyManager publicKeyManager, IRepositoryManager repositoryManager, IProjectManager projectManager, IFederationManager federationManager, @@ -119,6 +123,7 @@ public class GitBlitWebApp extends WebApplication { this.notificationManager = notificationManager; this.userManager = userManager; this.authenticationManager = authenticationManager; + this.publicKeyManager = publicKeyManager; this.repositoryManager = repositoryManager; this.projectManager = projectManager; this.federationManager = federationManager; @@ -280,6 +285,10 @@ public class GitBlitWebApp extends WebApplication { return authenticationManager; } + public IPublicKeyManager keys() { + return publicKeyManager; + } + public IRepositoryManager repositories() { return repositoryManager; } diff --git a/src/test/config/test-gitblit.properties b/src/test/config/test-gitblit.properties index 7d8e9a79..1a52eaf4 100644 --- a/src/test/config/test-gitblit.properties +++ b/src/test/config/test-gitblit.properties @@ -8,7 +8,7 @@ git.searchRepositoriesSubfolders = true git.enableGitServlet = true git.daemonPort = 8300 git.sshPort = 29418 -git.sshPublicKeyAuthenticator = com.gitblit.tests.BogusPublicKeyAuthenticator +git.sshKeysManager = com.gitblit.transport.ssh.MemoryKeyManager groovy.scriptsFolder = src/main/distrib/data/groovy groovy.preReceiveScripts = blockpush groovy.postReceiveScripts = sendmail diff --git a/src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java b/src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java deleted file mode 100644 index 80be1a01..00000000 --- a/src/test/java/com/gitblit/tests/BogusPublicKeyAuthenticator.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.tests; - -import java.security.PublicKey; - -import org.apache.sshd.server.session.ServerSession; - -import com.gitblit.manager.IAuthenticationManager; -import com.gitblit.transport.ssh.CachingPublicKeyAuthenticator; -import com.gitblit.transport.ssh.IKeyManager; - -public class BogusPublicKeyAuthenticator extends CachingPublicKeyAuthenticator { - - public BogusPublicKeyAuthenticator(IKeyManager keyManager, - IAuthenticationManager authManager) { - super(keyManager, authManager); - } - - @Override - protected boolean doAuthenticate(String username, PublicKey suppliedKey, - ServerSession session) { - // TODO(davido): put authenticated user in session - return true; - } -} diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java index 17d609e7..b8d3b181 100644 --- a/src/test/java/com/gitblit/tests/GitBlitSuite.java +++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java @@ -61,7 +61,7 @@ import com.gitblit.utils.JGitUtils; MarkdownUtilsTest.class, JGitUtilsTest.class, SyndicationUtilsTest.class, DiffUtilsTest.class, MetricUtilsTest.class, X509UtilsTest.class, GitBlitTest.class, FederationTests.class, RpcTests.class, GitServletTest.class, GitDaemonTest.class, - GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class, SshDaemonTest.class, + SshDaemonTest.class, GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class, FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class, ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class, BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class }) @@ -78,20 +78,12 @@ public class GitBlitSuite { static int port = 8280; static int gitPort = 8300; static int shutdownPort = 8281; - static int sshPort = 29418; - -// Overriding of keys doesn't seem to work -// static { -// try { -// sshPort = SshUtils.getFreePort(); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } + static int sshPort = 39418; public static String url = "http://localhost:" + port; public static String gitServletUrl = "http://localhost:" + port + "/git"; public static String gitDaemonUrl = "git://localhost:" + gitPort; + public static String sshDaemonUrl = "ssh://admin@localhost:" + sshPort; public static String account = "admin"; public static String password = "admin"; @@ -149,9 +141,7 @@ public class GitBlitSuite { "" + shutdownPort, "--gitPort", "" + gitPort, "--repositoriesFolder", "\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"", "--userService", GitBlitSuite.USERSCONF.getAbsolutePath(), "--settings", GitBlitSuite.SETTINGS.getAbsolutePath(), - "--baseFolder", "data"); - // doesn't work - //, "--sshPort", "" + sshPort); + "--baseFolder", "data", "--sshPort", "" + sshPort); } }); diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java index 5294f691..45d31c29 100644 --- a/src/test/java/com/gitblit/tests/SshDaemonTest.java +++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java @@ -26,11 +26,15 @@ import org.apache.sshd.ClientChannel; import org.apache.sshd.ClientSession; import org.apache.sshd.SshClient; import org.apache.sshd.common.KeyPairProvider; +import org.junit.After; import org.junit.AfterClass; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import com.gitblit.Constants; +import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.transport.ssh.MemoryKeyManager; public class SshDaemonTest extends GitblitUnitTest { @@ -50,6 +54,27 @@ public class SshDaemonTest extends GitblitUnitTest { } } + protected MemoryKeyManager getKeyManager() { + IPublicKeyManager mgr = gitblit().getPublicKeyManager(); + if (mgr instanceof MemoryKeyManager) { + return (MemoryKeyManager) gitblit().getPublicKeyManager(); + } else { + throw new RuntimeException("unexpected key manager type " + mgr.getClass().getName()); + } + } + + @Before + public void prepare() { + MemoryKeyManager keyMgr = getKeyManager(); + keyMgr.addKey("admin", pair.getPublic()); + } + + @After + public void tearDown() { + MemoryKeyManager keyMgr = getKeyManager(); + keyMgr.removeAllKeys("admin"); + } + @Test public void testPublicKeyAuthentication() throws Exception { SshClient client = SshClient.setUpDefaultClient(); -- cgit v1.2.3 From 7f3682a7448c6c4f59d7b66b3eab81fcf2999d0d Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 17 Mar 2014 21:54:07 -0400 Subject: Create a simple ls-users admin command --- .../ssh/gitblit/GitblitDispatchCommand.java | 1 + .../transport/ssh/gitblit/LsUsersCommand.java | 73 ++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java index 544b204f..9eff9bd1 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java @@ -32,6 +32,7 @@ public class GitblitDispatchCommand extends DispatchCommand { registerCommand(user, ReviewCommand.class); // administrative commands + registerCommand(user, LsUsersCommand.class); registerCommand(user, CreateRepository.class); registerCommand(user, SetAccountCommand.class); } diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java new file mode 100644 index 00000000..dd269213 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import java.text.MessageFormat; +import java.util.List; + +import org.kohsuke.args4j.Option; +import org.parboiled.common.StringUtils; + +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.SshCommand; + +@CommandMetaData(name = "ls-users", description = "List users", admin = true) +public class LsUsersCommand extends SshCommand { + + @Option(name = "--verbose", aliases = { "-v" }, usage = "verbose") + private boolean verbose; + + @Override + public void run() { + IGitblit gitblit = ctx.getGitblit(); + List users = gitblit.getAllUsers(); + int displaynameLen = 0; + int usernameLen = 0; + for (UserModel user : users) { + int len = user.getDisplayName().length(); + if (len > displaynameLen) { + displaynameLen = len; + } + if (!StringUtils.isEmpty(user.username)) { + len = user.username.length(); + if (len > usernameLen) { + usernameLen = len; + } + } + } + + String pattern; + if (verbose) { + pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s\t%-10s\t%s", displaynameLen, usernameLen); + } else { + pattern = MessageFormat.format("%-{0,number,0}s\t%-{1,number,0}s", displaynameLen, usernameLen); + } + + for (UserModel user : users) { + if (user.disabled) { + continue; + } + stdout.println(String.format(pattern, + user.getDisplayName(), + (user.canAdmin() ? "*":" ") + user.username, + user.accountType, + user.emailAddress == null ? "" : user.emailAddress)); + } + } +} -- cgit v1.2.3 From 261ddf0fcf9a55fbb5b4e7c6c2cdb4c2f8c860fe Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 17 Mar 2014 22:27:58 -0400 Subject: Revise dispatchers and move command classes --- .../com/gitblit/transport/ssh/CommandMetaData.java | 33 --- .../gitblit/transport/ssh/SshCommandContext.java | 43 --- .../gitblit/transport/ssh/SshCommandFactory.java | 292 --------------------- .../java/com/gitblit/transport/ssh/SshDaemon.java | 1 + .../com/gitblit/transport/ssh/WelcomeShell.java | 1 + .../transport/ssh/commands/BaseCommand.java | 1 - .../transport/ssh/commands/CommandMetaData.java | 33 +++ .../transport/ssh/commands/DispatchCommand.java | 12 +- .../transport/ssh/commands/RootDispatcher.java | 50 ++++ .../transport/ssh/commands/SshCommandContext.java | 44 ++++ .../transport/ssh/commands/SshCommandFactory.java | 272 +++++++++++++++++++ .../transport/ssh/git/GitDispatchCommand.java | 61 ----- .../gitblit/transport/ssh/git/GitDispatcher.java | 61 +++++ .../com/gitblit/transport/ssh/git/Receive.java | 2 +- .../java/com/gitblit/transport/ssh/git/Upload.java | 2 +- .../transport/ssh/gitblit/AddKeyCommand.java | 2 +- .../transport/ssh/gitblit/CreateRepository.java | 2 +- .../ssh/gitblit/GitblitDispatchCommand.java | 39 --- .../transport/ssh/gitblit/GitblitDispatcher.java | 39 +++ .../gitblit/transport/ssh/gitblit/LsCommand.java | 2 +- .../transport/ssh/gitblit/LsUsersCommand.java | 2 +- .../transport/ssh/gitblit/RemoveKeyCommand.java | 2 +- .../transport/ssh/gitblit/ReviewCommand.java | 2 +- .../transport/ssh/gitblit/SetAccountCommand.java | 2 +- .../transport/ssh/gitblit/VersionCommand.java | 2 +- 25 files changed, 516 insertions(+), 486 deletions(-) delete mode 100644 src/main/java/com/gitblit/transport/ssh/CommandMetaData.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshCommandContext.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/SshCommandContext.java create mode 100644 src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java delete mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java create mode 100644 src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java deleted file mode 100644 index 0d39f33f..00000000 --- a/src/main/java/com/gitblit/transport/ssh/CommandMetaData.java +++ /dev/null @@ -1,33 +0,0 @@ -//Copyright (C) 2013 The Android Open Source Project -// -//Licensed 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 com.gitblit.transport.ssh; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** -* Annotation tagged on a concrete Command to describe what it is doing -*/ -@Target({ElementType.TYPE}) -@Retention(RUNTIME) -public @interface CommandMetaData { -String name(); -String description() default ""; -boolean admin() default false; -boolean hidden() default false; -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java b/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java deleted file mode 100644 index 163d0795..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandContext.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import com.gitblit.manager.IGitblit; - -public class SshCommandContext { - - private final IGitblit gitblit; - private final SshDaemonClient client; - private final String commandLine; - - public SshCommandContext(IGitblit gitblit, SshDaemonClient client, String commandLine) { - this.gitblit = gitblit; - this.client = client; - this.commandLine = commandLine; - } - - public IGitblit getGitblit() { - return gitblit; - } - - public SshDaemonClient getClient() { - return client; - } - - public String getCommandLine() { - return commandLine; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java deleted file mode 100644 index 2b2093ea..00000000 --- a/src/main/java/com/gitblit/transport/ssh/SshCommandFactory.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import org.apache.sshd.server.Command; -import org.apache.sshd.server.CommandFactory; -import org.apache.sshd.server.Environment; -import org.apache.sshd.server.ExitCallback; -import org.apache.sshd.server.SessionAware; -import org.apache.sshd.server.session.ServerSession; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.manager.IGitblit; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.git.GitDispatchCommand; -import com.gitblit.transport.ssh.gitblit.GitblitDispatchCommand; -import com.gitblit.utils.IdGenerator; -import com.gitblit.utils.WorkQueue; -import com.google.common.util.concurrent.Atomics; - -/** - * - * @author Eric Myhre - * - */ -public class SshCommandFactory implements CommandFactory { - private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class); - - private final IGitblit gitblit; - private final ScheduledExecutorService startExecutor; - - public SshCommandFactory(IGitblit gitblit, IdGenerator idGenerator) { - this.gitblit = gitblit; - - int threads = 2;// cfg.getInt("sshd","commandStartThreads", 2); - WorkQueue workQueue = new WorkQueue(idGenerator); - startExecutor = workQueue.createQueue(threads, "SshCommandStart"); - } - - /** - * Creates the root dispatcher command which builds up the available commands. - * - * @param the client - * @param the command line - * @return the root dispatcher command - */ - protected DispatchCommand createRootDispatcher(SshDaemonClient client, String cmdLine) { - final UserModel user = client.getUser(); - - DispatchCommand root = new DispatchCommand() { - }; - root.setContext(new SshCommandContext(gitblit, client, cmdLine)); - - // TODO convert these dispatchers to plugin extension points - root.registerDispatcher(user, GitblitDispatchCommand.class); - root.registerDispatcher(user, GitDispatchCommand.class); - - return root; - } - - @Override - public Command createCommand(final String commandLine) { - return new Trampoline(commandLine); - } - - private class Trampoline implements Command, SessionAware { - private final String[] argv; - private ServerSession session; - private InputStream in; - private OutputStream out; - private OutputStream err; - private ExitCallback exit; - private Environment env; - private String cmdLine; - private DispatchCommand cmd; - private final AtomicBoolean logged; - private final AtomicReference> task; - - Trampoline(String line) { - if (line.startsWith("git-")) { - line = "git " + line; - } - cmdLine = line; - argv = split(line); - logged = new AtomicBoolean(); - task = Atomics.newReference(); - } - - @Override - public void setSession(ServerSession session) { - this.session = session; - } - - @Override - public void setInputStream(final InputStream in) { - this.in = in; - } - - @Override - public void setOutputStream(final OutputStream out) { - this.out = out; - } - - @Override - public void setErrorStream(final OutputStream err) { - this.err = err; - } - - @Override - public void setExitCallback(final ExitCallback callback) { - this.exit = callback; - } - - @Override - public void start(final Environment env) throws IOException { - this.env = env; - task.set(startExecutor.submit(new Runnable() { - @Override - public void run() { - try { - onStart(); - } catch (Exception e) { - logger.warn("Cannot start command ", e); - } - } - - @Override - public String toString() { - return "start (user " + session.getUsername() + ")"; - } - })); - } - - private void onStart() throws IOException { - synchronized (this) { - SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); - try { - cmd = createRootDispatcher(client, cmdLine); - cmd.setArguments(argv); - cmd.setInputStream(in); - cmd.setOutputStream(out); - cmd.setErrorStream(err); - cmd.setExitCallback(new ExitCallback() { - @Override - public void onExit(int rc, String exitMessage) { - exit.onExit(translateExit(rc), exitMessage); - log(rc); - } - - @Override - public void onExit(int rc) { - exit.onExit(translateExit(rc)); - log(rc); - } - }); - cmd.start(env); - } finally { - client = null; - } - } - } - - private int translateExit(final int rc) { - return rc; - // - // switch (rc) { - // case BaseCommand.STATUS_NOT_ADMIN: - // return 1; - // - // case BaseCommand.STATUS_CANCEL: - // return 15 /* SIGKILL */; - // - // case BaseCommand.STATUS_NOT_FOUND: - // return 127 /* POSIX not found */; - // - // default: - // return rc; - // } - - } - - private void log(final int rc) { - if (logged.compareAndSet(false, true)) { - // log.onExecute(cmd, rc); - logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc); - } - } - - @Override - public void destroy() { - Future future = task.getAndSet(null); - if (future != null) { - future.cancel(true); - // destroyExecutor.execute(new Runnable() { - // @Override - // public void run() { - // onDestroy(); - // } - // }); - } - } - - @SuppressWarnings("unused") - private void onDestroy() { - synchronized (this) { - if (cmd != null) { - // final Context old = sshScope.set(ctx); - try { - cmd.destroy(); - // log(BaseCommand.STATUS_CANCEL); - } finally { - // ctx = null; - cmd = null; - // sshScope.set(old); - } - } - } - } - } - - /** Split a command line into a string array. */ - static public String[] split(String commandLine) { - final List list = new ArrayList(); - boolean inquote = false; - boolean inDblQuote = false; - StringBuilder r = new StringBuilder(); - for (int ip = 0; ip < commandLine.length();) { - final char b = commandLine.charAt(ip++); - switch (b) { - case '\t': - case ' ': - if (inquote || inDblQuote) - r.append(b); - else if (r.length() > 0) { - list.add(r.toString()); - r = new StringBuilder(); - } - continue; - case '\"': - if (inquote) - r.append(b); - else - inDblQuote = !inDblQuote; - continue; - case '\'': - if (inDblQuote) - r.append(b); - else - inquote = !inquote; - continue; - case '\\': - if (inquote || ip == commandLine.length()) - r.append(b); // literal within a quote - else - r.append(commandLine.charAt(ip++)); - continue; - default: - r.append(b); - continue; - } - } - if (r.length() > 0) { - list.add(r.toString()); - } - return list.toArray(new String[list.size()]); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java index da9a3726..aeb6ce51 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java +++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java @@ -34,6 +34,7 @@ import com.gitblit.Constants; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.manager.IGitblit; +import com.gitblit.transport.ssh.commands.SshCommandFactory; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.StringUtils; diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java index 819028c5..ccf2586b 100644 --- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java +++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java @@ -35,6 +35,7 @@ import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.SshCommandFactory; import com.gitblit.utils.StringUtils; /** diff --git a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java index a3df787c..7088fefa 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java @@ -37,7 +37,6 @@ import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.transport.ssh.SshCommandContext; import com.gitblit.utils.IdGenerator; import com.gitblit.utils.WorkQueue; import com.gitblit.utils.WorkQueue.CancelableRunnable; diff --git a/src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java new file mode 100644 index 00000000..133b9cbf --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java @@ -0,0 +1,33 @@ +//Copyright (C) 2013 The Android Open Source Project +// +//Licensed 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 com.gitblit.transport.ssh.commands; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** +* Annotation tagged on a concrete Command to describe what it is doing +*/ +@Target({ElementType.TYPE}) +@Retention(RUNTIME) +public @interface CommandMetaData { +String name(); +String description() default ""; +boolean admin() default false; +boolean hidden() default false; +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java index dd581f4d..f7c78d28 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java @@ -31,7 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; import com.gitblit.utils.StringUtils; import com.gitblit.utils.cli.SubcommandHandler; import com.google.common.base.Charsets; @@ -48,15 +47,15 @@ public abstract class DispatchCommand extends BaseCommand { @Argument(index = 1, multiValued = true, metaVar = "ARG") private List args = new ArrayList(); - private Set> commands; + private final Set> commands; private Map> map; private Map dispatchers; - public DispatchCommand() { + protected DispatchCommand() { commands = new HashSet>(); } - public void registerDispatcher(UserModel user, Class cmd) { + protected void registerDispatcher(UserModel user, Class cmd) { if (!cmd.isAnnotationPresent(CommandMetaData.class)) { throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(), CommandMetaData.class.getName())); @@ -80,8 +79,7 @@ public abstract class DispatchCommand extends BaseCommand { } } - protected void registerCommands(UserModel user) { - } + protected abstract void registerCommands(UserModel user); /** @@ -90,7 +88,7 @@ public abstract class DispatchCommand extends BaseCommand { * @param user * @param cmd */ - public void registerCommand(UserModel user, Class cmd) { + protected void registerCommand(UserModel user, Class cmd) { if (!cmd.isAnnotationPresent(CommandMetaData.class)) { throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(), CommandMetaData.class.getName())); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java new file mode 100644 index 00000000..5d9eb197 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.SshDaemonClient; +import com.gitblit.transport.ssh.git.GitDispatcher; +import com.gitblit.transport.ssh.gitblit.GitblitDispatcher; + +/** + * The root dispatcher is the dispatch command that handles registering all + * other commands. + * + */ +public class RootDispatcher extends DispatchCommand { + + public RootDispatcher(IGitblit gitblit, SshDaemonClient client, String cmdLine) { + super(); + setContext(new SshCommandContext(gitblit, client, cmdLine)); + + final UserModel user = client.getUser(); + registerDispatcher(user, GitblitDispatcher.class); + registerDispatcher(user, GitDispatcher.class); + + // TODO register plugin dispatchers here + } + + @Override + protected final void registerCommands(UserModel user) { + } + + @Override + protected final void registerCommand(UserModel user, Class cmd) { + throw new RuntimeException("The root dispatcher does not accept commands, only dispatchers!"); + } +} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommandContext.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandContext.java new file mode 100644 index 00000000..15f7a8fe --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandContext.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import com.gitblit.manager.IGitblit; +import com.gitblit.transport.ssh.SshDaemonClient; + +public class SshCommandContext { + + private final IGitblit gitblit; + private final SshDaemonClient client; + private final String commandLine; + + public SshCommandContext(IGitblit gitblit, SshDaemonClient client, String commandLine) { + this.gitblit = gitblit; + this.client = client; + this.commandLine = commandLine; + } + + public IGitblit getGitblit() { + return gitblit; + } + + public SshDaemonClient getClient() { + return client; + } + + public String getCommandLine() { + return commandLine; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java new file mode 100644 index 00000000..3eefcae7 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java @@ -0,0 +1,272 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.server.Command; +import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.manager.IGitblit; +import com.gitblit.transport.ssh.SshDaemonClient; +import com.gitblit.utils.IdGenerator; +import com.gitblit.utils.WorkQueue; +import com.google.common.util.concurrent.Atomics; + +/** + * + * @author Eric Myhre + * + */ +public class SshCommandFactory implements CommandFactory { + private static final Logger logger = LoggerFactory.getLogger(SshCommandFactory.class); + + private final IGitblit gitblit; + private final ScheduledExecutorService startExecutor; + + public SshCommandFactory(IGitblit gitblit, IdGenerator idGenerator) { + this.gitblit = gitblit; + + int threads = 2;// cfg.getInt("sshd","commandStartThreads", 2); + WorkQueue workQueue = new WorkQueue(idGenerator); + startExecutor = workQueue.createQueue(threads, "SshCommandStart"); + } + + public RootDispatcher createRootDispatcher(SshDaemonClient client, String commandLine) { + return new RootDispatcher(gitblit, client, commandLine); + } + + @Override + public Command createCommand(final String commandLine) { + return new Trampoline(commandLine); + } + + private class Trampoline implements Command, SessionAware { + private final String[] argv; + private ServerSession session; + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback exit; + private Environment env; + private String cmdLine; + private DispatchCommand cmd; + private final AtomicBoolean logged; + private final AtomicReference> task; + + Trampoline(String line) { + if (line.startsWith("git-")) { + line = "git " + line; + } + cmdLine = line; + argv = split(line); + logged = new AtomicBoolean(); + task = Atomics.newReference(); + } + + @Override + public void setSession(ServerSession session) { + this.session = session; + } + + @Override + public void setInputStream(final InputStream in) { + this.in = in; + } + + @Override + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + @Override + public void setErrorStream(final OutputStream err) { + this.err = err; + } + + @Override + public void setExitCallback(final ExitCallback callback) { + this.exit = callback; + } + + @Override + public void start(final Environment env) throws IOException { + this.env = env; + task.set(startExecutor.submit(new Runnable() { + @Override + public void run() { + try { + onStart(); + } catch (Exception e) { + logger.warn("Cannot start command ", e); + } + } + + @Override + public String toString() { + return "start (user " + session.getUsername() + ")"; + } + })); + } + + private void onStart() throws IOException { + synchronized (this) { + SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); + try { + cmd = createRootDispatcher(client, cmdLine); + cmd.setArguments(argv); + cmd.setInputStream(in); + cmd.setOutputStream(out); + cmd.setErrorStream(err); + cmd.setExitCallback(new ExitCallback() { + @Override + public void onExit(int rc, String exitMessage) { + exit.onExit(translateExit(rc), exitMessage); + log(rc); + } + + @Override + public void onExit(int rc) { + exit.onExit(translateExit(rc)); + log(rc); + } + }); + cmd.start(env); + } finally { + client = null; + } + } + } + + private int translateExit(final int rc) { + return rc; + // + // switch (rc) { + // case BaseCommand.STATUS_NOT_ADMIN: + // return 1; + // + // case BaseCommand.STATUS_CANCEL: + // return 15 /* SIGKILL */; + // + // case BaseCommand.STATUS_NOT_FOUND: + // return 127 /* POSIX not found */; + // + // default: + // return rc; + // } + + } + + private void log(final int rc) { + if (logged.compareAndSet(false, true)) { + // log.onExecute(cmd, rc); + logger.info("onExecute: {} exits with: {}", cmd.getClass().getSimpleName(), rc); + } + } + + @Override + public void destroy() { + Future future = task.getAndSet(null); + if (future != null) { + future.cancel(true); + // destroyExecutor.execute(new Runnable() { + // @Override + // public void run() { + // onDestroy(); + // } + // }); + } + } + + @SuppressWarnings("unused") + private void onDestroy() { + synchronized (this) { + if (cmd != null) { + // final Context old = sshScope.set(ctx); + try { + cmd.destroy(); + // log(BaseCommand.STATUS_CANCEL); + } finally { + // ctx = null; + cmd = null; + // sshScope.set(old); + } + } + } + } + } + + /** Split a command line into a string array. */ + static public String[] split(String commandLine) { + final List list = new ArrayList(); + boolean inquote = false; + boolean inDblQuote = false; + StringBuilder r = new StringBuilder(); + for (int ip = 0; ip < commandLine.length();) { + final char b = commandLine.charAt(ip++); + switch (b) { + case '\t': + case ' ': + if (inquote || inDblQuote) + r.append(b); + else if (r.length() > 0) { + list.add(r.toString()); + r = new StringBuilder(); + } + continue; + case '\"': + if (inquote) + r.append(b); + else + inDblQuote = !inDblQuote; + continue; + case '\'': + if (inDblQuote) + r.append(b); + else + inquote = !inquote; + continue; + case '\\': + if (inquote || ip == commandLine.length()) + r.append(b); // literal within a quote + else + r.append(commandLine.charAt(ip++)); + continue; + default: + r.append(b); + continue; + } + } + if (r.length() > 0) { + list.add(r.toString()); + } + return list.toArray(new String[list.size()]); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java deleted file mode 100644 index adeace52..00000000 --- a/src/main/java/com/gitblit/transport/ssh/git/GitDispatchCommand.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.git; - -import com.gitblit.git.GitblitReceivePackFactory; -import com.gitblit.git.GitblitUploadPackFactory; -import com.gitblit.git.RepositoryResolver; -import com.gitblit.manager.IGitblit; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.SshCommandContext; -import com.gitblit.transport.ssh.SshDaemonClient; -import com.gitblit.transport.ssh.commands.BaseCommand; -import com.gitblit.transport.ssh.commands.DispatchCommand; - -@CommandMetaData(name = "git", description="Dispatcher for git receive and upload commands", hidden = true) -public class GitDispatchCommand extends DispatchCommand { - - protected RepositoryResolver repositoryResolver; - protected GitblitUploadPackFactory uploadPackFactory; - protected GitblitReceivePackFactory receivePackFactory; - - @Override - public void setContext(SshCommandContext context) { - super.setContext(context); - - IGitblit gitblit = context.getGitblit(); - repositoryResolver = new RepositoryResolver(gitblit); - uploadPackFactory = new GitblitUploadPackFactory(gitblit); - receivePackFactory = new GitblitReceivePackFactory(gitblit); - } - - @Override - protected void registerCommands(UserModel user) { - registerCommand(user, Upload.class); - registerCommand(user, Receive.class); - } - - @Override - protected void provideStateTo(final BaseCommand cmd) { - super.provideStateTo(cmd); - - BaseGitCommand a = (BaseGitCommand) cmd; - a.setRepositoryResolver(repositoryResolver); - a.setUploadPackFactory(uploadPackFactory); - a.setReceivePackFactory(receivePackFactory); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java b/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java new file mode 100644 index 00000000..fa1dfbd4 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.git; + +import com.gitblit.git.GitblitReceivePackFactory; +import com.gitblit.git.GitblitUploadPackFactory; +import com.gitblit.git.RepositoryResolver; +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.SshDaemonClient; +import com.gitblit.transport.ssh.commands.BaseCommand; +import com.gitblit.transport.ssh.commands.CommandMetaData; +import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.SshCommandContext; + +@CommandMetaData(name = "git", description="Dispatcher for git receive and upload commands", hidden = true) +public class GitDispatcher extends DispatchCommand { + + protected RepositoryResolver repositoryResolver; + protected GitblitUploadPackFactory uploadPackFactory; + protected GitblitReceivePackFactory receivePackFactory; + + @Override + public void setContext(SshCommandContext context) { + super.setContext(context); + + IGitblit gitblit = context.getGitblit(); + repositoryResolver = new RepositoryResolver(gitblit); + uploadPackFactory = new GitblitUploadPackFactory(gitblit); + receivePackFactory = new GitblitReceivePackFactory(gitblit); + } + + @Override + protected void registerCommands(UserModel user) { + registerCommand(user, Upload.class); + registerCommand(user, Receive.class); + } + + @Override + protected void provideStateTo(final BaseCommand cmd) { + super.provideStateTo(cmd); + + BaseGitCommand a = (BaseGitCommand) cmd; + a.setRepositoryResolver(repositoryResolver); + a.setUploadPackFactory(uploadPackFactory); + a.setReceivePackFactory(receivePackFactory); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/git/Receive.java b/src/main/java/com/gitblit/transport/ssh/git/Receive.java index 4089f1df..9597eb4d 100644 --- a/src/main/java/com/gitblit/transport/ssh/git/Receive.java +++ b/src/main/java/com/gitblit/transport/ssh/git/Receive.java @@ -17,7 +17,7 @@ package com.gitblit.transport.ssh.git; import org.eclipse.jgit.transport.ReceivePack; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; @CommandMetaData(name = "git-receive-pack", description = "Receives pushes from a client") public class Receive extends BaseGitCommand { diff --git a/src/main/java/com/gitblit/transport/ssh/git/Upload.java b/src/main/java/com/gitblit/transport/ssh/git/Upload.java index 5793c3e6..5de6b4d2 100644 --- a/src/main/java/com/gitblit/transport/ssh/git/Upload.java +++ b/src/main/java/com/gitblit/transport/ssh/git/Upload.java @@ -17,7 +17,7 @@ package com.gitblit.transport.ssh.git; import org.eclipse.jgit.transport.UploadPack; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; @CommandMetaData(name = "git-upload-pack", description = "Sends packs to a client for clone and fetch") public class Upload extends BaseGitCommand { diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java index 6d5c85c7..ae24dfb3 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/AddKeyCommand.java @@ -23,7 +23,7 @@ import org.kohsuke.args4j.Argument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; /** * Add a key to the current user's authorized keys list. diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java index b2e1b1b0..2917b6d2 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/CreateRepository.java @@ -18,7 +18,7 @@ package com.gitblit.transport.ssh.gitblit; import org.kohsuke.args4j.Option; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; import com.gitblit.transport.ssh.commands.SshCommand; @CommandMetaData(name = "create-repository", description = "Create new GIT repository", admin = true, hidden = true) diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java deleted file mode 100644 index 9eff9bd1..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatchCommand.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; - -@CommandMetaData(name = "gitblit", description = "Gitblit server commands") -public class GitblitDispatchCommand extends DispatchCommand { - - @Override - protected void registerCommands(UserModel user) { - // normal usage commands - registerCommand(user, VersionCommand.class); - registerCommand(user, AddKeyCommand.class); - registerCommand(user, RemoveKeyCommand.class); - registerCommand(user, LsCommand.class); - registerCommand(user, ReviewCommand.class); - - // administrative commands - registerCommand(user, LsUsersCommand.class); - registerCommand(user, CreateRepository.class); - registerCommand(user, SetAccountCommand.class); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java new file mode 100644 index 00000000..eb3bb0c6 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.gitblit; + +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.commands.CommandMetaData; +import com.gitblit.transport.ssh.commands.DispatchCommand; + +@CommandMetaData(name = "gitblit", description = "Gitblit server commands") +public class GitblitDispatcher extends DispatchCommand { + + @Override + protected void registerCommands(UserModel user) { + // normal usage commands + registerCommand(user, VersionCommand.class); + registerCommand(user, AddKeyCommand.class); + registerCommand(user, RemoveKeyCommand.class); + registerCommand(user, LsCommand.class); + registerCommand(user, ReviewCommand.class); + + // administrative commands + registerCommand(user, LsUsersCommand.class); + registerCommand(user, CreateRepository.class); + registerCommand(user, SetAccountCommand.class); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java index cf50a2ee..75cb2d83 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/LsCommand.java @@ -27,7 +27,7 @@ import com.gitblit.manager.IGitblit; import com.gitblit.models.ProjectModel; import com.gitblit.models.RepositoryModel; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; import com.gitblit.transport.ssh.commands.SshCommand; @CommandMetaData(name = "ls", description = "List repositories or projects") diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java index dd269213..752afaf6 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/LsUsersCommand.java @@ -24,7 +24,7 @@ import org.parboiled.common.StringUtils; import com.gitblit.manager.IGitblit; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; import com.gitblit.transport.ssh.commands.SshCommand; @CommandMetaData(name = "ls-users", description = "List users", admin = true) diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java index 7c9abfd4..af307303 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/RemoveKeyCommand.java @@ -23,7 +23,7 @@ import org.kohsuke.args4j.Argument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; /** diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java index 9e4d8ba7..b3691cbb 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java @@ -25,7 +25,7 @@ import com.gitblit.models.TicketModel.Change; import com.gitblit.models.TicketModel.Patchset; import com.gitblit.models.TicketModel.Score; import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; import com.gitblit.transport.ssh.commands.SshCommand; import com.gitblit.wicket.GitBlitWebSession; diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java index 28ac9e19..aebe3b1f 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/SetAccountCommand.java @@ -22,7 +22,7 @@ import java.util.List; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; /** Set a user's account settings. **/ @CommandMetaData(name = "set-account", description = "Change an account's settings", admin = true) diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java index 513f6d96..384c6ce4 100644 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java @@ -17,7 +17,7 @@ package com.gitblit.transport.ssh.gitblit; import com.gitblit.Constants; -import com.gitblit.transport.ssh.CommandMetaData; +import com.gitblit.transport.ssh.commands.CommandMetaData; import com.gitblit.transport.ssh.commands.SshCommand; @CommandMetaData(name="version", description = "Display the Gitblit version") -- cgit v1.2.3 From 84f406bfd20ec2076cf7616e7f396ad480513bc4 Mon Sep 17 00:00:00 2001 From: David Ostrovsky Date: Mon, 10 Mar 2014 01:50:49 +0100 Subject: Add plugins/extension infrastructure Plugins are stored in `${baseFolder}/plugins` and are loaded during startup by the PluginManager. A plugin defines it's metadata in META-INF/MANIFEST.MF: Plugin-Class: com.gitblit.plugins.cookbook.CookbookPlugin Plugin-Dependencies: foo, bar Plugin-Id: gitblit-plugin Plugin-Provider: John Doe Plugin-Version: 1.0 Plugins can define extension points that can be implemented by other plugins and they can depend on other plugins: Plugin-Dependencies: foo, bar During the load phase, a directed acyclic graph is built and the loading order of the dependency chain is reversed using a topological sort; parent followed by children. The parent plugin classloader is the combined classloader of all parent plugins. Change-Id: I738821fa2bff02a5dbe339a944cc7e3c4dd8e299 --- .classpath | 1 + build.moxie | 1 + gitblit.iml | 11 +++++ src/main/distrib/data/gitblit.properties | 8 ++++ src/main/java/WEB-INF/web.xml | 3 +- src/main/java/com/gitblit/DaggerModule.java | 13 ++++- src/main/java/com/gitblit/FederationClient.java | 2 +- src/main/java/com/gitblit/GitBlit.java | 7 ++- src/main/java/com/gitblit/dagger/DaggerFilter.java | 2 +- .../java/com/gitblit/manager/GitblitManager.java | 18 ++++++- src/main/java/com/gitblit/manager/IGitblit.java | 3 +- .../java/com/gitblit/manager/IPluginManager.java | 39 +++++++++++++++ .../java/com/gitblit/manager/PluginManager.java | 56 ++++++++++++++++++++++ .../java/com/gitblit/servlet/GitblitContext.java | 2 + .../com/gitblit/servlet/SyndicationFilter.java | 1 + 15 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/gitblit/manager/IPluginManager.java create mode 100644 src/main/java/com/gitblit/manager/PluginManager.java (limited to 'src') diff --git a/.classpath b/.classpath index d3aec7e8..252a7c96 100644 --- a/.classpath +++ b/.classpath @@ -76,6 +76,7 @@ + diff --git a/build.moxie b/build.moxie index 6015becc..eb2878a8 100644 --- a/build.moxie +++ b/build.moxie @@ -174,6 +174,7 @@ dependencies: - compile 'args4j:args4j:2.0.26' :war :fedclient :authority - compile 'commons-codec:commons-codec:1.7' :war - compile 'redis.clients:jedis:2.3.1' :war +- compile 'ro.fortsoft.pf4j:pf4j:0.6' :war - test 'junit' # Dependencies for Selenium web page testing - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar diff --git a/gitblit.iml b/gitblit.iml index a48f12ad..ed067f28 100644 --- a/gitblit.iml +++ b/gitblit.iml @@ -790,6 +790,17 @@ + + + + + + + + + + + diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 64a52f5c..762344b4 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -1844,3 +1844,11 @@ server.requireClientCertificates = false # SINCE 0.5.0 # RESTART REQUIRED server.shutdownPort = 8081 + +# Base folder for plugins. +# This folder may contain Gitblit plugins +# +# SINCE 1.6.0 +# RESTART REQUIRED +# BASEFOLDER +plugins.folder = ${baseFolder}/plugins diff --git a/src/main/java/WEB-INF/web.xml b/src/main/java/WEB-INF/web.xml index 1451ec63..77456d47 100644 --- a/src/main/java/WEB-INF/web.xml +++ b/src/main/java/WEB-INF/web.xml @@ -199,7 +199,6 @@ /robots.txt - + + + + + + + + diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index 0001706c..6b1cc8a5 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -42,7 +42,9 @@ import org.eclipse.jgit.transport.RefSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ro.fortsoft.pf4j.PluginClassLoader; import ro.fortsoft.pf4j.PluginWrapper; +import ro.fortsoft.pf4j.RuntimeMode; import com.gitblit.Constants; import com.gitblit.Constants.AccessPermission; @@ -1187,4 +1189,54 @@ public class GitblitManager implements IGitblit { public PluginWrapper whichPlugin(Class clazz) { return pluginManager.whichPlugin(clazz); } + + @Override + public boolean deletePlugin(PluginWrapper wrapper) { + return pluginManager.deletePlugin(wrapper); + } + + @Override + public List getPlugins() { + return pluginManager.getPlugins(); + } + + @Override + public List getResolvedPlugins() { + return pluginManager.getResolvedPlugins(); + } + + @Override + public List getUnresolvedPlugins() { + return pluginManager.getUnresolvedPlugins(); + } + + @Override + public List getStartedPlugins() { + return pluginManager.getStartedPlugins(); + } + + @Override + public void loadPlugins() { + pluginManager.loadPlugins(); + } + + @Override + public void startPlugins() { + pluginManager.startPlugins(); + } + + @Override + public void stopPlugins() { + pluginManager.stopPlugins(); + } + + @Override + public PluginClassLoader getPluginClassLoader(String pluginId) { + return pluginManager.getPluginClassLoader(pluginId); + } + + @Override + public RuntimeMode getRuntimeMode() { + return pluginManager.getRuntimeMode(); + } } diff --git a/src/main/java/com/gitblit/manager/IPluginManager.java b/src/main/java/com/gitblit/manager/IPluginManager.java index 670e9769..11b81ea3 100644 --- a/src/main/java/com/gitblit/manager/IPluginManager.java +++ b/src/main/java/com/gitblit/manager/IPluginManager.java @@ -15,19 +15,10 @@ */ package com.gitblit.manager; -import java.util.List; - +import ro.fortsoft.pf4j.PluginManager; import ro.fortsoft.pf4j.PluginWrapper; -public interface IPluginManager extends IManager { - - /** - * Retrieves the extension for given class 'clazz'. - * - * @param clazz extension point class to retrieve extension for - * @return list of extensions - */ - public List getExtensions(Class clazz); +public interface IPluginManager extends IManager, PluginManager { /** * Retrieves the {@link PluginWrapper} that loaded the given class 'clazz'. @@ -35,5 +26,13 @@ public interface IPluginManager extends IManager { * @param clazz extension point class to retrieve extension for * @return PluginWrapper that loaded the given class */ - public PluginWrapper whichPlugin(Class clazz); + PluginWrapper whichPlugin(Class clazz); + + /** + * Delete the plugin represented by {@link PluginWrapper}. + * + * @param wrapper + * @return true if successful + */ + boolean deletePlugin(PluginWrapper wrapper); } diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index 5eb00e92..e23aaec0 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -15,12 +15,16 @@ */ package com.gitblit.manager; +import java.io.File; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.fortsoft.pf4j.DefaultPluginManager; +import ro.fortsoft.pf4j.PluginWrapper; import com.gitblit.Keys; +import com.gitblit.utils.FileUtils; /** * The plugin manager maintains the lifecycle of plugins. It is exposed as @@ -30,27 +34,45 @@ import com.gitblit.Keys; * @author David Ostrovsky * */ -public class PluginManager extends DefaultPluginManager implements - IPluginManager { +public class PluginManager extends DefaultPluginManager implements IPluginManager { private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final IRuntimeManager runtimeManager; public PluginManager(IRuntimeManager runtimeManager) { - super(runtimeManager.getFileOrFolder(Keys.plugins.folder, - "${baseFolder}/plugins")); + super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins")); + this.runtimeManager = runtimeManager; } @Override public PluginManager start() { - logger.info("Plugin manager started"); + logger.info("Loading plugins..."); loadPlugins(); + logger.info("Starting loaded plugins..."); startPlugins(); return this; } @Override public PluginManager stop() { + logger.info("Stopping loaded plugins..."); stopPlugins(); return null; } + + @Override + public boolean deletePlugin(PluginWrapper pw) { + File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + File pluginFolder = new File(folder, pw.getPluginPath()); + File pluginZip = new File(folder, pw.getPluginPath() + ".zip"); + + if (pluginFolder.exists()) { + FileUtils.delete(pluginFolder); + } + if (pluginZip.exists()) { + FileUtils.delete(pluginZip); + } + return true; + } } diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java index 6809ba62..4341a3ea 100644 --- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java +++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java @@ -165,7 +165,7 @@ public class WelcomeShell implements Factory { msg.append(nl); msg.append(nl); - msg.append(String.format(" cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s gitblit keys add", user.username, port, hostname)); + msg.append(String.format(" cat ~/.ssh/id_rsa.pub | ssh -l %s -p %d %s keys add", user.username, port, hostname)); msg.append(nl); msg.append(nl); diff --git a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java new file mode 100644 index 00000000..5c413db2 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java @@ -0,0 +1,293 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import java.util.ArrayList; +import java.util.List; + +import org.kohsuke.args4j.Argument; + +import ro.fortsoft.pf4j.PluginDependency; +import ro.fortsoft.pf4j.PluginDescriptor; +import ro.fortsoft.pf4j.PluginState; +import ro.fortsoft.pf4j.PluginWrapper; + +import com.gitblit.manager.IGitblit; +import com.gitblit.models.UserModel; +import com.gitblit.utils.FlipTable; +import com.gitblit.utils.FlipTable.Borders; + +/** + * The plugin dispatcher and commands for runtime plugin management. + * + * @author James Moger + * + */ +@CommandMetaData(name = "plugin", description = "Plugin management commands", admin = true) +public class PluginDispatcher extends DispatchCommand { + + @Override + protected void setup(UserModel user) { + register(user, ListPlugins.class); + register(user, StartPlugin.class); + register(user, StopPlugin.class); + register(user, ShowPlugin.class); + register(user, RemovePlugin.class); + register(user, UploadPlugin.class); + } + + @CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins") + public static class ListPlugins extends ListCommand { + + @Override + protected List getItems() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + List list = gitblit.getPlugins(); + return list; + } + + @Override + protected void asTable(List list) { + String[] headers; + if (verbose) { + String [] h = { "#", "Id", "Version", "State", "Mode", "Path", "Provider"}; + headers = h; + } else { + String [] h = { "#", "Id", "Version", "State", "Path"}; + headers = h; + } + Object[][] data = new Object[list.size()][]; + for (int i = 0; i < list.size(); i++) { + PluginWrapper p = list.get(i); + PluginDescriptor d = p.getDescriptor(); + if (verbose) { + data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getRuntimeMode(), p.getPluginPath(), d.getProvider() }; + } else { + data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getPluginPath() }; + } + } + + stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); + } + + @Override + protected void asTabbed(List list) { + for (PluginWrapper pw : list) { + PluginDescriptor d = pw.getDescriptor(); + if (verbose) { + outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getRuntimeMode(), pw.getPluginPath(), d.getProvider()); + } else { + outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath()); + } + } + } + } + + @CommandMetaData(name = "start", description = "Start a plugin") + public static class StartPlugin extends SshCommand { + + @Argument(index = 0, required = true, metaVar = "ALL|", usage = "the plugin to start") + protected String plugin; + + @Override + public void run() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + if (plugin.equalsIgnoreCase("ALL")) { + gitblit.startPlugins(); + stdout.println("All plugins started"); + } else { + try { + int index = Integer.parseInt(plugin); + List plugins = gitblit.getPlugins(); + if (index > plugins.size()) { + throw new UnloggedFailure(1, "Invalid plugin index specified!"); + } + PluginWrapper pw = plugins.get(index - 1); + start(pw); + } catch (NumberFormatException n) { + for (PluginWrapper pw : gitblit.getPlugins()) { + PluginDescriptor pd = pw.getDescriptor(); + if (pd.getPluginId().equalsIgnoreCase(plugin)) { + start(pw); + break; + } + } + } + } + } + + protected void start(PluginWrapper pw) throws UnloggedFailure { + String id = pw.getDescriptor().getPluginId(); + if (pw.getPluginState() == PluginState.STARTED) { + throw new UnloggedFailure(1, String.format("%s is already started.", id)); + } + try { + pw.getPlugin().start(); +// pw.setPluginState(PluginState.STARTED); + stdout.println(String.format("%s started", id)); + } catch (Exception pe) { + throw new UnloggedFailure(1, String.format("Failed to start %s", id), pe); + } + } + } + + + @CommandMetaData(name = "stop", description = "Stop a plugin") + public static class StopPlugin extends SshCommand { + + @Argument(index = 0, required = true, metaVar = "ALL|", usage = "the plugin to stop") + protected String plugin; + + @Override + public void run() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + if (plugin.equalsIgnoreCase("ALL")) { + gitblit.stopPlugins(); + stdout.println("All plugins stopped"); + } else { + try { + int index = Integer.parseInt(plugin); + List plugins = gitblit.getPlugins(); + if (index > plugins.size()) { + throw new UnloggedFailure(1, "Invalid plugin index specified!"); + } + PluginWrapper pw = plugins.get(index - 1); + stop(pw); + } catch (NumberFormatException n) { + for (PluginWrapper pw : gitblit.getPlugins()) { + PluginDescriptor pd = pw.getDescriptor(); + if (pd.getPluginId().equalsIgnoreCase(plugin)) { + stop(pw); + break; + } + } + } + } + } + + protected void stop(PluginWrapper pw) throws UnloggedFailure { + String id = pw.getDescriptor().getPluginId(); + if (pw.getPluginState() == PluginState.STOPPED) { + throw new UnloggedFailure(1, String.format("%s is already stopped.", id)); + } + try { + pw.getPlugin().stop(); +// pw.setPluginState(PluginState.STOPPED); + stdout.println(String.format("%s stopped", id)); + } catch (Exception pe) { + throw new UnloggedFailure(1, String.format("Failed to stop %s", id), pe); + } + } + } + + @CommandMetaData(name = "show", description = "Show the details of a plugin") + public static class ShowPlugin extends SshCommand { + + @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to stop") + protected int index; + + @Override + public void run() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + List plugins = gitblit.getPlugins(); + if (index > plugins.size()) { + throw new UnloggedFailure(1, "Invalid plugin index specified!"); + } + PluginWrapper pw = plugins.get(index - 1); + PluginDescriptor d = pw.getDescriptor(); + + // FIELDS + StringBuilder sb = new StringBuilder(); + sb.append("Version : ").append(d.getVersion()).append('\n'); + sb.append("Provider : ").append(d.getProvider()).append('\n'); + sb.append("Path : ").append(pw.getPluginPath()).append('\n'); + sb.append("State : ").append(pw.getPluginState()).append('\n'); + final String fields = sb.toString(); + + // TODO EXTENSIONS + sb.setLength(0); + List exts = new ArrayList(); + String extensions; + if (exts.isEmpty()) { + extensions = FlipTable.EMPTY; + } else { + String[] headers = { "Id", "Version" }; + Object[][] data = new Object[exts.size()][]; + for (int i = 0; i < exts.size(); i++) { + String ext = exts.get(i); + data[0] = new Object[] { ext.toString(), ext.toString() }; + } + extensions = FlipTable.of(headers, data, Borders.COLS); + } + + // DEPENDENCIES + sb.setLength(0); + List deps = d.getDependencies(); + String dependencies; + if (deps.isEmpty()) { + dependencies = FlipTable.EMPTY; + } else { + String[] headers = { "Id", "Version" }; + Object[][] data = new Object[deps.size()][]; + for (int i = 0; i < deps.size(); i++) { + PluginDependency dep = deps.get(i); + data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() }; + } + dependencies = FlipTable.of(headers, data, Borders.COLS); + } + + String[] headers = { d.getPluginId() }; + Object[][] data = new Object[5][]; + data[0] = new Object[] { fields }; + data[1] = new Object[] { "EXTENSIONS" }; + data[2] = new Object[] { extensions }; + data[3] = new Object[] { "DEPENDENCIES" }; + data[4] = new Object[] { dependencies }; + stdout.println(FlipTable.of(headers, data)); + } + } + + @CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true) + public static class RemovePlugin extends SshCommand { + + @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to stop") + protected int index; + + @Override + public void run() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + List plugins = gitblit.getPlugins(); + if (index > plugins.size()) { + throw new UnloggedFailure(1, "Invalid plugin index specified!"); + } + PluginWrapper pw = plugins.get(index - 1); + PluginDescriptor d = pw.getDescriptor(); + if (gitblit.deletePlugin(pw)) { + stdout.println(String.format("Deleted %s %s", d.getPluginId(), d.getVersion())); + } else { + throw new UnloggedFailure(1, String.format("Failed to delete %s %s", d.getPluginId(), d.getVersion())); + } + } + } + + @CommandMetaData(name = "receive", aliases= { "upload" }, description = "Upload a plugin to the server", hidden = true) + public static class UploadPlugin extends SshCommand { + + @Override + public void run() throws UnloggedFailure { + } + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java index 8a871ebb..3c378669 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java @@ -24,7 +24,7 @@ import com.gitblit.manager.IGitblit; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.SshDaemonClient; import com.gitblit.transport.ssh.git.GitDispatcher; -import com.gitblit.transport.ssh.gitblit.GitblitDispatcher; +import com.gitblit.transport.ssh.keys.KeysDispatcher; /** * The root dispatcher is the dispatch command that handles registering all @@ -41,8 +41,10 @@ class RootDispatcher extends DispatchCommand { setContext(new SshCommandContext(gitblit, client, cmdLine)); UserModel user = client.getUser(); - register(user, GitblitDispatcher.class); + register(user, VersionCommand.class); register(user, GitDispatcher.class); + register(user, KeysDispatcher.class); + register(user, PluginDispatcher.class); List exts = gitblit.getExtensions(DispatchCommand.class); for (DispatchCommand ext : exts) { diff --git a/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java new file mode 100644 index 00000000..3a2fd5e2 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.commands; + +import com.gitblit.Constants; + +@CommandMetaData(name="version", description = "Display the Gitblit version") +public class VersionCommand extends SshCommand { + + @Override + public void run() { + stdout.println(Constants.getGitBlitVersion()); + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java deleted file mode 100644 index 930c058f..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/BaseKeyCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.util.List; - -import com.gitblit.transport.ssh.IPublicKeyManager; -import com.gitblit.transport.ssh.SshKey; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.google.common.base.Charsets; - -/** - * - * Base class for commands that read SSH keys from stdin or a parameter list. - * - */ -abstract class BaseKeyCommand extends SshCommand { - - protected List readKeys(List sshKeys) - throws UnsupportedEncodingException, IOException { - int idx = -1; - if (sshKeys.isEmpty() || (idx = sshKeys.indexOf("-")) >= 0) { - String sshKey = ""; - BufferedReader br = new BufferedReader(new InputStreamReader( - in, Charsets.UTF_8)); - String line; - while ((line = br.readLine()) != null) { - sshKey += line + "\n"; - } - if (idx == -1) { - sshKeys.add(sshKey.trim()); - } else { - sshKeys.set(idx, sshKey.trim()); - } - } - return sshKeys; - } - - protected IPublicKeyManager getKeyManager() { - return getContext().getGitblit().getPublicKeyManager(); - } - - protected SshKey parseKey(String rawData) throws UnloggedFailure { - if (rawData.contains("PRIVATE")) { - throw new UnloggedFailure(1, "Please provide a PUBLIC key, not a PRIVATE key!"); - } - SshKey key = new SshKey(rawData); - return key; - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java deleted file mode 100644 index f6740349..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/ConfigCommand.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.gitblit.transport.ssh.gitblit; - -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; -import org.parboiled.common.StringUtils; - -import com.gitblit.manager.IGitblit; -import com.gitblit.models.ServerSettings; -import com.gitblit.models.SettingModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.gitblit.transport.ssh.commands.UsageExample; -import com.gitblit.transport.ssh.commands.UsageExamples; -import com.google.common.collect.Maps; - -@CommandMetaData(name = "config", description = "Administer Gitblit settings", admin = true) -@UsageExamples(examples = { - @UsageExample(syntax = "${cmd} --list", description = "List all settings"), - @UsageExample(syntax = "${cmd} git.sshPort", description = "Describe the git.sshPort setting"), - @UsageExample(syntax = "${cmd} git.sshPort 29418", description = "Set git.sshPort to 29418"), - @UsageExample(syntax = "${cmd} git.sshPort --reset", description = "Reset git.sshPort to it's default value"), -}) -public class ConfigCommand extends SshCommand { - - @Argument(index = 0, metaVar = "KEY", usage = "The setting to describe or update") - protected String setting; - - @Argument(index = 1, metaVar = "VALUE", usage = "The new value for the setting") - protected String value; - - @Option(name = "--list", aliases = { "-l" }, usage = "List all settings") - private boolean listAll; - - @Option(name = "--modified", aliases = { "-m" }, usage = "List modified settings") - private boolean listModified; - - @Option(name = "--reset", usage = "Reset a setting to it's default value") - private boolean reset; - - @Override - public void run() throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - ServerSettings settings = gitblit.getSettingsModel(); - - if (listAll || listModified) { - /* - * List settings - */ - List list = new ArrayList(); - int maxLen = 0; - for (String key : settings.getKeys()) { - SettingModel model = settings.get(key); - if (listModified) { - if (!model.isDefaultValue()) { - list.add(model); - } else { - continue; - } - } else { - list.add(model); - } - - if (key.length() > maxLen) { - maxLen = key.length(); - } - } - String pattern = MessageFormat.format("%s%-{0,number,0}s : %s", maxLen); - for (SettingModel model : list) { - stdout.println(String.format(pattern, - model.isDefaultValue() ? " " : "*", - model.name, - model.currentValue)); - } - } else if (!StringUtils.isEmpty(setting) && value == null && !reset) { - /* - * Describe a setting - */ - SettingModel model = settings.get(setting); - if (model == null) { - // unknown setting - String value = gitblit.getSettings().getString(setting, null); - if (value == null) { - // setting does not exist, can not describe - stdout.println(String.format("\"%s\" is not a valid setting.", setting)); - return; - } - - model = new SettingModel(); - model.defaultValue = ""; - model.currentValue = value; - } - stdout.println(); - stdout.println(model.name); - if (!StringUtils.isEmpty(model.since)) { - stdout.println(SettingModel.SINCE + " " + model.since); - } - if (model.restartRequired) { - stdout.println(SettingModel.RESTART_REQUIRED); - } - if (model.spaceDelimited) { - stdout.println(SettingModel.SPACE_DELIMITED); - } - if (!StringUtils.isEmpty(model.description)) { - stdout.println(); - stdout.println(model.description); - } - stdout.println(); - if (model.defaultValue != null) { - stdout.println("default: " + model.defaultValue); - } - if (!model.isDefaultValue()) { - stdout.println("current: " + model.currentValue); - } else { - stdout.println("current: "); - } - stdout.println(); - } else if (!StringUtils.isEmpty(setting) && value == null && reset) { - /* - * Reset a setting - */ - SettingModel model = settings.get(setting); - if (model == null) { - stdout.println(String.format("\"%s\" is not a valid setting.", setting)); - return; - } - - if (model.defaultValue == null || model.defaultValue.equals("null")) { - // no default value, remove setting - gitblit.getSettings().removeSetting(setting); - gitblit.getSettings().saveSettings(); - settings.remove(setting); - - stdout.println(String.format("%s removed.", setting)); - } else { - // reset to default value - Map updates = Maps.newHashMap(); - updates.put(setting, model.defaultValue == null ? "" : model.defaultValue); - gitblit.getSettings().saveSettings(updates); - - // confirm reset - String newValue = gitblit.getSettings().getString(setting, null); - if (model.defaultValue.equals(newValue)) { - stdout.println(String.format("%s reset to the default value.", setting)); - } else { - stdout.println(String.format("failed to reset %s!", setting)); - } - } - - } else if (!StringUtils.isEmpty(setting) && value != null) { - /* - * Update a setting - */ - Map updates = Maps.newHashMap(); - updates.put(setting, value); - gitblit.getSettings().saveSettings(updates); - - // confirm update - String newValue = gitblit.getSettings().getString(setting, null); - if (value.equals(newValue)) { - stdout.println(String.format("%s updated.", setting)); - } else { - stdout.println(String.format("failed to update %s!", setting)); - } - } else { - // Display usage - showHelp(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java deleted file mode 100644 index 67fedeaa..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/GitblitDispatcher.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; - -@CommandMetaData(name = "gitblit", description = "Gitblit server commands") -public class GitblitDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - // commands in this dispatcher - register(user, VersionCommand.class); - register(user, ConfigCommand.class); - - // nested dispatchers - register(user, ListDispatcher.class); - register(user, KeysDispatcher.class); - register(user, TicketsDispatcher.class); - register(user, UsersDispatcher.class); - register(user, TeamsDispatcher.class); - register(user, ProjectsDispatcher.class); - register(user, RepositoriesDispatcher.class); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java deleted file mode 100644 index 9bb60003..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/KeysDispatcher.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.IPublicKeyManager; -import com.gitblit.transport.ssh.SshKey; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.gitblit.transport.ssh.commands.UsageExample; -import com.gitblit.utils.FlipTable; -import com.gitblit.utils.FlipTable.Borders; -import com.google.common.base.Joiner; - -/** - * The dispatcher and it's commands for SSH public key management. - * - * @author James Moger - * - */ -@CommandMetaData(name = "keys", description = "SSH public key management commands") -public class KeysDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - register(user, AddKey.class); - register(user, RemoveKey.class); - register(user, ListKeys.class); - register(user, WhichKey.class); - register(user, CommentKey.class); - } - - @CommandMetaData(name = "add", description = "Add an SSH public key to your account") - @UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd} -", description = "Upload your SSH public key and add it to your account") - public static class AddKey extends BaseKeyCommand { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - @Argument(metaVar = "", usage = "the key(s) to add") - private List addKeys = new ArrayList(); - - @Override - public void run() throws IOException, UnloggedFailure { - String username = getContext().getClient().getUsername(); - List keys = readKeys(addKeys); - for (String key : keys) { - SshKey sshKey = parseKey(key); - getKeyManager().addKey(username, sshKey); - log.info("added SSH public key for {}", username); - } - } - } - - @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove an SSH public key from your account") - @UsageExample(syntax = "${cmd} 2", description = "Remove the SSH key identified as #2 in `keys list`") - public static class RemoveKey extends BaseKeyCommand { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - - private final String ALL = "ALL"; - - @Argument(metaVar = "||ALL", usage = "the key to remove", required = true) - private List removeKeys = new ArrayList(); - - @Override - public void run() throws IOException, UnloggedFailure { - String username = getContext().getClient().getUsername(); - // remove a key that has been piped to the command - // or remove all keys - - List currentKeys = getKeyManager().getKeys(username); - if (currentKeys == null || currentKeys.isEmpty()) { - throw new UnloggedFailure(1, "There are no registered keys!"); - } - - List keys = readKeys(removeKeys); - if (keys.contains(ALL)) { - if (getKeyManager().removeAllKeys(username)) { - stdout.println("Removed all keys."); - log.info("removed all SSH public keys from {}", username); - } else { - log.warn("failed to remove all SSH public keys from {}", username); - } - } else { - for (String key : keys) { - try { - // remove a key by it's index (1-based indexing) - int index = Integer.parseInt(key); - if (index > keys.size()) { - if (keys.size() == 1) { - throw new UnloggedFailure(1, "Invalid index specified. There is only 1 registered key."); - } - throw new UnloggedFailure(1, String.format("Invalid index specified. There are %d registered keys.", keys.size())); - } - SshKey sshKey = currentKeys.get(index - 1); - if (getKeyManager().removeKey(username, sshKey)) { - stdout.println(String.format("Removed %s", sshKey.getFingerprint())); - } else { - throw new UnloggedFailure(1, String.format("failed to remove #%s: %s", key, sshKey.getFingerprint())); - } - } catch (Exception e) { - // remove key by raw key data - SshKey sshKey = parseKey(key); - if (getKeyManager().removeKey(username, sshKey)) { - stdout.println(String.format("Removed %s", sshKey.getFingerprint())); - log.info("removed SSH public key {} from {}", sshKey.getFingerprint(), username); - } else { - log.warn("failed to remove SSH public key {} from {}", sshKey.getFingerprint(), username); - throw new UnloggedFailure(1, String.format("failed to remove %s", sshKey.getFingerprint())); - } - } - } - } - } - } - - @CommandMetaData(name = "list", aliases = { "ls" }, description = "List your registered SSH public keys") - public static class ListKeys extends SshCommand { - - @Option(name = "-L", usage = "list complete public key parameters") - private boolean showRaw; - - @Override - public void run() { - IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager(); - String username = getContext().getClient().getUsername(); - List keys = keyManager.getKeys(username); - - if (showRaw) { - asRaw(keys); - } else { - asTable(keys); - } - } - - /* output in the same format as authorized_keys */ - protected void asRaw(List keys) { - if (keys == null) { - return; - } - for (SshKey key : keys) { - stdout.println(key.getRawData()); - } - } - - protected void asTable(List keys) { - String[] headers = { "#", "Fingerprint", "Comment", "Type" }; - int len = keys == null ? 0 : keys.size(); - Object[][] data = new Object[len][]; - for (int i = 0; i < len; i++) { - // show 1-based index numbers with the fingerprint - // this is useful for comparing with "ssh-add -l" - SshKey k = keys.get(i); - data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), k.getAlgorithm() }; - } - - stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - } - } - - @CommandMetaData(name = "which", description = "Display the SSH public key used for this session") - public static class WhichKey extends SshCommand { - - @Option(name = "-L", usage = "list complete public key parameters") - private boolean showRaw; - - @Override - public void run() throws UnloggedFailure { - SshKey key = getContext().getClient().getKey(); - if (key == null) { - throw new UnloggedFailure(1, "You have not authenticated with an SSH public key."); - } - - if (showRaw) { - stdout.println(key.getRawData()); - } else { - final String username = getContext().getClient().getUsername(); - List keys = getContext().getGitblit().getPublicKeyManager().getKeys(username); - int index = 0; - for (int i = 0; i < keys.size(); i++) { - if (key.equals(keys.get(i))) { - index = i + 1; - break; - } - } - asTable(index, key); - } - } - - protected void asTable(int index, SshKey key) { - String[] headers = { "#", "Fingerprint", "Comment", "Type" }; - Object[][] data = new Object[1][]; - data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getAlgorithm() }; - - stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - } - } - - @CommandMetaData(name = "comment", description = "Set the comment for an SSH public key") - @UsageExample(syntax = "${cmd} 3 Home workstation", description = "Set the comment for key #3") - public static class CommentKey extends SshCommand { - - @Argument(index = 0, metaVar = "INDEX", usage = "the key index", required = true) - private int index; - - @Argument(index = 1, metaVar = "COMMENT", usage = "the new comment", required = true) - private List values = new ArrayList(); - - @Override - public void run() throws UnloggedFailure { - final String username = getContext().getClient().getUsername(); - IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager(); - List keys = keyManager.getKeys(username); - if (index > keys.size()) { - throw new UnloggedFailure(1, "Invalid key index!"); - } - - String comment = Joiner.on(" ").join(values); - SshKey key = keys.get(index - 1); - key.setComment(comment); - if (keyManager.addKey(username, key)) { - stdout.println(String.format("Updated the comment for key #%d.", index)); - } else { - throw new UnloggedFailure(1, String.format("Failed to update the comment for key #%d!", index)); - } - } - - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java deleted file mode 100644 index 343e59aa..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/ListDispatcher.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; - -/** - * The dispatcher and it's commands for Gitblit object listing. - * - * @author James Moger - * - */ -@CommandMetaData(name = "list", aliases = { "ls" }, description = "Gitblit object list commands") -public class ListDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - register(user, ListRepositories.class); - register(user, ListProjects.class); - register(user, ListUsers.class); - register(user, ListKeys.class); - } - - /* List SSH public keys */ - @CommandMetaData(name = "keys", description = "List your public keys") - public static class ListKeys extends KeysDispatcher.ListKeys { - } - - /* List repositories */ - @CommandMetaData(name = "repositories", aliases = { "repos" }, description = "List repositories") - public static class ListRepositories extends RepositoriesDispatcher.ListRepositories { - } - - /* List projects */ - @CommandMetaData(name = "projects", description = "List projects") - public static class ListProjects extends ProjectsDispatcher.ListProjects { - } - - /* List users */ - @CommandMetaData(name = "users", description = "List users", admin = true) - public static class ListUsers extends UsersDispatcher.ListUsers { - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java deleted file mode 100644 index 97076adf..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/ProjectsDispatcher.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.util.List; - -import com.gitblit.manager.IGitblit; -import com.gitblit.models.ProjectModel; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.ListFilterCommand; -import com.gitblit.utils.FlipTable; -import com.gitblit.utils.FlipTable.Borders; - -@CommandMetaData(name = "projects", description = "Project management commands") -public class ProjectsDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - register(user, ListProjects.class); - } - - /* List projects */ - @CommandMetaData(name = "list", aliases= { "ls" }, description = "List projects") - public static class ListProjects extends ListFilterCommand { - - @Override - protected List getItems() { - IGitblit gitblit = getContext().getGitblit(); - UserModel user = getContext().getClient().getUser(); - - List projects = gitblit.getProjectModels(user, false); - return projects; - } - - @Override - protected boolean matches(String filter, ProjectModel p) { - return p.name.matches(filter); - } - - @Override - protected void asTable(List list) { - String[] headers; - if (verbose) { - String[] h = { "Name", "Description", "Last Modified", "# Repos" }; - headers = h; - } else { - String[] h = { "Name", "Last Modified", "# Repos" }; - headers = h; - } - - Object[][] data = new Object[list.size()][]; - for (int i = 0; i < list.size(); i++) { - ProjectModel p = list.get(i); - - if (verbose) { - data[i] = new Object[] { p.name, p.description, formatDate(p.lastChange), p.repositories.size() }; - } else { - data[i] = new Object[] { p.name, formatDate(p.lastChange), p.repositories.size() }; - } - } - stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - } - - @Override - protected void asTabbed(List list) { - if (verbose) { - for (ProjectModel project : list) { - outTabbed(project.name, - project.description == null ? "" : project.description, - formatDate(project.lastChange)); - } - } else { - for (ProjectModel project : list) { - outTabbed(project.name); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java deleted file mode 100644 index 292c2126..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/RepositoriesDispatcher.java +++ /dev/null @@ -1,532 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.kohsuke.args4j.Argument; - -import com.gitblit.GitBlitException; -import com.gitblit.Keys; -import com.gitblit.Constants.AccessRestrictionType; -import com.gitblit.Constants.AuthorizationControl; -import com.gitblit.manager.IGitblit; -import com.gitblit.models.RegistrantAccessPermission; -import com.gitblit.models.RepositoryModel; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.ListFilterCommand; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.gitblit.transport.ssh.commands.UsageExample; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.FlipTable; -import com.gitblit.utils.FlipTable.Borders; -import com.gitblit.utils.StringUtils; -import com.google.common.base.Joiner; - -@CommandMetaData(name = "repositories", aliases = { "repos" }, description = "Repository management commands") -public class RepositoriesDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - // primary commands - register(user, NewRepository.class); - register(user, RenameRepository.class); - register(user, RemoveRepository.class); - register(user, ShowRepository.class); - register(user, ListRepositories.class); - - // repository-specific commands - register(user, SetField.class); - } - - public static abstract class RepositoryCommand extends SshCommand { - @Argument(index = 0, required = true, metaVar = "REPOSITORY", usage = "repository") - protected String repository; - - protected RepositoryModel getRepository(boolean requireRepository) throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - RepositoryModel repo = gitblit.getRepositoryModel(repository); - if (requireRepository && repo == null) { - throw new UnloggedFailure(1, String.format("Repository %s does not exist!", repository)); - } - return repo; - } - - protected String sanitize(String name) throws UnloggedFailure { - // automatically convert backslashes to forward slashes - name = name.replace('\\', '/'); - // Automatically replace // with / - name = name.replace("//", "/"); - - // prohibit folder paths - if (name.startsWith("/")) { - throw new UnloggedFailure(1, "Illegal leading slash"); - } - if (name.startsWith("../")) { - throw new UnloggedFailure(1, "Illegal relative slash"); - } - if (name.contains("/../")) { - throw new UnloggedFailure(1, "Illegal relative slash"); - } - if (name.endsWith("/")) { - name = name.substring(0, name.length() - 1); - } - return name; - } - } - - @CommandMetaData(name = "new", aliases = { "add" }, description = "Create a new repository") - @UsageExample(syntax = "${cmd} myRepo") - public static class NewRepository extends RepositoryCommand { - - @Override - public void run() throws UnloggedFailure { - - UserModel user = getContext().getClient().getUser(); - - String name = sanitize(repository); - - if (!user.canCreate(name)) { - // try to prepend personal path - String path = StringUtils.getFirstPathElement(name); - if ("".equals(path)) { - name = user.getPersonalPath() + "/" + name; - } - } - - if (getRepository(false) != null) { - throw new UnloggedFailure(1, String.format("Repository %s already exists!", name)); - } - - if (!user.canCreate(name)) { - throw new UnloggedFailure(1, String.format("Sorry, you do not have permission to create %s", name)); - } - - IGitblit gitblit = getContext().getGitblit(); - - RepositoryModel repo = new RepositoryModel(); - repo.name = name; - repo.projectPath = StringUtils.getFirstPathElement(name); - String restriction = gitblit.getSettings().getString(Keys.git.defaultAccessRestriction, "PUSH"); - repo.accessRestriction = AccessRestrictionType.fromName(restriction); - String authorization = gitblit.getSettings().getString(Keys.git.defaultAuthorizationControl, null); - repo.authorizationControl = AuthorizationControl.fromName(authorization); - - if (user.isMyPersonalRepository(name)) { - // personal repositories are private by default - repo.addOwner(user.username); - repo.accessRestriction = AccessRestrictionType.VIEW; - repo.authorizationControl = AuthorizationControl.NAMED; - } - - try { - gitblit.updateRepositoryModel(repository, repo, true); - stdout.println(String.format("%s created.", repo.name)); - } catch (GitBlitException e) { - log.error("Failed to add " + repository, e); - throw new UnloggedFailure(1, e.getMessage()); - } - } - } - - @CommandMetaData(name = "rename", aliases = { "mv" }, description = "Rename a repository") - @UsageExample(syntax = "${cmd} myRepo.git otherRepo.git", description = "Rename the repository from myRepo.git to otherRepo.git") - public static class RenameRepository extends RepositoryCommand { - @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "the new repository name") - protected String newRepositoryName; - - @Override - public void run() throws UnloggedFailure { - RepositoryModel repo = getRepository(true); - IGitblit gitblit = getContext().getGitblit(); - UserModel user = getContext().getClient().getUser(); - - String name = sanitize(newRepositoryName); - if (!user.canCreate(name)) { - // try to prepend personal path - String path = StringUtils.getFirstPathElement(name); - if ("".equals(path)) { - name = user.getPersonalPath() + "/" + name; - } - } - - if (null != gitblit.getRepositoryModel(name)) { - throw new UnloggedFailure(1, String.format("Repository %s already exists!", name)); - } - - if (repo.name.equalsIgnoreCase(name)) { - throw new UnloggedFailure(1, "Repository names are identical"); - } - - if (!user.canAdmin(repo)) { - throw new UnloggedFailure(1, String.format("Sorry, you do not have permission to rename %s", repository)); - } - - if (!user.canCreate(name)) { - throw new UnloggedFailure(1, String.format("Sorry, you don't have permission to move %s to %s/", repository, name)); - } - - // set the new name - repo.name = name; - - try { - gitblit.updateRepositoryModel(repository, repo, false); - stdout.println(String.format("Renamed repository %s to %s.", repository, name)); - } catch (GitBlitException e) { - String msg = String.format("Failed to rename repository from %s to %s", repository, name); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - } - - @CommandMetaData(name = "set", description = "Set the specified field of a repository") - @UsageExample(syntax = "${cmd} myRepo description John's personal projects", description = "Set the description of a repository") - public static class SetField extends RepositoryCommand { - - @Argument(index = 1, required = true, metaVar = "FIELD", usage = "the field to update") - protected String fieldName; - - @Argument(index = 2, required = true, metaVar = "VALUE", usage = "the new value") - protected List fieldValues = new ArrayList(); - - protected enum Field { - description; - - static Field fromString(String name) { - for (Field field : values()) { - if (field.name().equalsIgnoreCase(name)) { - return field; - } - } - return null; - } - } - - @Override - protected String getUsageText() { - String fields = Joiner.on(", ").join(Field.values()); - StringBuilder sb = new StringBuilder(); - sb.append("Valid fields are:\n ").append(fields); - return sb.toString(); - } - - @Override - public void run() throws UnloggedFailure { - RepositoryModel repo = getRepository(true); - - Field field = Field.fromString(fieldName); - if (field == null) { - throw new UnloggedFailure(1, String.format("Unknown field %s", fieldName)); - } - - if (!getContext().getClient().getUser().canAdmin(repo)) { - throw new UnloggedFailure(1, String.format("Sorry, you do not have permission to administer %s", repository)); - } - - String value = Joiner.on(" ").join(fieldValues).trim(); - IGitblit gitblit = getContext().getGitblit(); - - switch(field) { - case description: - repo.description = value; - break; - default: - throw new UnloggedFailure(1, String.format("Field %s was not properly handled by the set command.", fieldName)); - } - - try { - gitblit.updateRepositoryModel(repo.name, repo, false); - stdout.println(String.format("Set %s.%s = %s", repo.name, fieldName, value)); - } catch (GitBlitException e) { - String msg = String.format("Failed to set %s.%s = %s", repo.name, fieldName, value); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - - protected boolean toBool(String value) throws UnloggedFailure { - String v = value.toLowerCase(); - if (v.equals("t") - || v.equals("true") - || v.equals("yes") - || v.equals("on") - || v.equals("y") - || v.equals("1")) { - return true; - } else if (v.equals("f") - || v.equals("false") - || v.equals("no") - || v.equals("off") - || v.equals("n") - || v.equals("0")) { - return false; - } - throw new UnloggedFailure(1, String.format("Invalid boolean value %s", value)); - } - } - - @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove a repository") - @UsageExample(syntax = "${cmd} myRepo.git", description = "Delete myRepo.git") - public static class RemoveRepository extends RepositoryCommand { - - @Override - public void run() throws UnloggedFailure { - - RepositoryModel repo = getRepository(true); - - if (!getContext().getClient().getUser().canAdmin(repo)) { - throw new UnloggedFailure(1, String.format("Sorry, you do not have permission to delete %s", repository)); - } - - IGitblit gitblit = getContext().getGitblit(); - if (gitblit.deleteRepositoryModel(repo)) { - stdout.println(String.format("%s has been deleted.", repository)); - } else { - throw new UnloggedFailure(1, String.format("Failed to delete %s!", repository)); - } - } - } - - @CommandMetaData(name = "show", description = "Show the details of a repository") - @UsageExample(syntax = "${cmd} myRepo.git", description = "Display myRepo.git") - public static class ShowRepository extends RepositoryCommand { - - @Override - public void run() throws UnloggedFailure { - - RepositoryModel r = getRepository(true); - - if (!getContext().getClient().getUser().canAdmin(r)) { - throw new UnloggedFailure(1, String.format("Sorry, you do not have permission to see the %s settings.", repository)); - } - - IGitblit gitblit = getContext().getGitblit(); - - // fields - StringBuilder fb = new StringBuilder(); - fb.append("Description : ").append(toString(r.description)).append('\n'); - fb.append("Origin : ").append(toString(r.origin)).append('\n'); - fb.append("Default Branch : ").append(toString(r.HEAD)).append('\n'); - fb.append('\n'); - fb.append("GC Period : ").append(r.gcPeriod).append('\n'); - fb.append("GC Threshold : ").append(r.gcThreshold).append('\n'); - fb.append('\n'); - fb.append("Accept Tickets : ").append(toString(r.acceptNewTickets)).append('\n'); - fb.append("Accept Patchsets : ").append(toString(r.acceptNewPatchsets)).append('\n'); - fb.append("Require Approval : ").append(toString(r.requireApproval)).append('\n'); - fb.append("Merge To : ").append(toString(r.mergeTo)).append('\n'); - fb.append('\n'); - fb.append("Incremental push tags : ").append(toString(r.useIncrementalPushTags)).append('\n'); - fb.append("Show remote branches : ").append(toString(r.showRemoteBranches)).append('\n'); - fb.append("Skip size calculations : ").append(toString(r.skipSizeCalculation)).append('\n'); - fb.append("Skip summary metrics : ").append(toString(r.skipSummaryMetrics)).append('\n'); - fb.append("Max activity commits : ").append(r.maxActivityCommits).append('\n'); - fb.append("Author metric exclusions : ").append(toString(r.metricAuthorExclusions)).append('\n'); - fb.append("Commit Message Renderer : ").append(r.commitMessageRenderer).append('\n'); - fb.append("Mailing Lists : ").append(toString(r.mailingLists)).append('\n'); - fb.append('\n'); - fb.append("Access Restriction : ").append(r.accessRestriction).append('\n'); - fb.append("Authorization Control : ").append(r.authorizationControl).append('\n'); - fb.append('\n'); - fb.append("Is Frozen : ").append(toString(r.isFrozen)).append('\n'); - fb.append("Allow Forks : ").append(toString(r.allowForks)).append('\n'); - fb.append("Verify Committer : ").append(toString(r.verifyCommitter)).append('\n'); - fb.append('\n'); - fb.append("Federation Strategy : ").append(r.federationStrategy).append('\n'); - fb.append("Federation Sets : ").append(toString(r.federationSets)).append('\n'); - fb.append('\n'); - fb.append("Indexed Branches : ").append(toString(r.indexedBranches)).append('\n'); - fb.append('\n'); - fb.append("Pre-Receive Scripts : ").append(toString(r.preReceiveScripts)).append('\n'); - fb.append(" inherited : ").append(toString(gitblit.getPreReceiveScriptsInherited(r))).append('\n'); - fb.append("Post-Receive Scripts : ").append(toString(r.postReceiveScripts)).append('\n'); - fb.append(" inherited : ").append(toString(gitblit.getPostReceiveScriptsInherited(r))).append('\n'); - String fields = fb.toString(); - - // owners - String owners; - if (r.owners.isEmpty()) { - owners = FlipTable.EMPTY; - } else { - String[] pheaders = { "Account", "Name" }; - Object [][] pdata = new Object[r.owners.size()][]; - for (int i = 0; i < r.owners.size(); i++) { - String owner = r.owners.get(i); - UserModel u = gitblit.getUserModel(owner); - pdata[i] = new Object[] { owner, u == null ? "" : u.getDisplayName() }; - } - owners = FlipTable.of(pheaders, pdata, Borders.COLS); - } - - // team permissions - List tperms = gitblit.getTeamAccessPermissions(r); - String tpermissions; - if (tperms.isEmpty()) { - tpermissions = FlipTable.EMPTY; - } else { - String[] pheaders = { "Team", "Permission", "Type" }; - Object [][] pdata = new Object[tperms.size()][]; - for (int i = 0; i < tperms.size(); i++) { - RegistrantAccessPermission ap = tperms.get(i); - pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType }; - } - tpermissions = FlipTable.of(pheaders, pdata, Borders.COLS); - } - - // user permissions - List uperms = gitblit.getUserAccessPermissions(r); - String upermissions; - if (uperms.isEmpty()) { - upermissions = FlipTable.EMPTY; - } else { - String[] pheaders = { "Account", "Name", "Permission", "Type", "Source", "Mutable" }; - Object [][] pdata = new Object[uperms.size()][]; - for (int i = 0; i < uperms.size(); i++) { - RegistrantAccessPermission ap = uperms.get(i); - String name = ""; - try { - String dn = gitblit.getUserModel(ap.registrant).displayName; - if (dn != null) { - name = dn; - } - } catch (Exception e) { - } - pdata[i] = new Object[] { ap.registrant, name, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" }; - } - upermissions = FlipTable.of(pheaders, pdata, Borders.COLS); - } - - // assemble table - String title = r.name; - String [] headers = new String[] { title }; - String[][] data = new String[8][]; - data[0] = new String [] { "FIELDS" }; - data[1] = new String [] {fields }; - data[2] = new String [] { "OWNERS" }; - data[3] = new String [] { owners }; - data[4] = new String [] { "TEAM PERMISSIONS" }; - data[5] = new String [] { tpermissions }; - data[6] = new String [] { "USER PERMISSIONS" }; - data[7] = new String [] { upermissions }; - stdout.println(FlipTable.of(headers, data)); - } - - protected String toString(String val) { - if (val == null) { - return ""; - } - return val; - } - - protected String toString(Collection collection) { - if (collection == null) { - return ""; - } - return Joiner.on(", ").join(collection); - } - - protected String toString(boolean val) { - if (val) { - return "Y"; - } - return ""; - } - - } - - /* List repositories */ - @CommandMetaData(name = "list", aliases = { "ls" }, description = "List repositories") - @UsageExample(syntax = "${cmd} mirror/.* -v", description = "Verbose list of all repositories in the 'mirror' directory") - public static class ListRepositories extends ListFilterCommand { - - @Override - protected List getItems() { - IGitblit gitblit = getContext().getGitblit(); - UserModel user = getContext().getClient().getUser(); - List repositories = gitblit.getRepositoryModels(user); - return repositories; - } - - @Override - protected boolean matches(String filter, RepositoryModel r) { - return r.name.matches(filter); - } - - @Override - protected void asTable(List list) { - String[] headers; - if (verbose) { - String[] h = { "Name", "Description", "Owners", "Last Modified", "Size" }; - headers = h; - } else { - String[] h = { "Name", "Last Modified", "Size" }; - headers = h; - } - - Object[][] data = new Object[list.size()][]; - for (int i = 0; i < list.size(); i++) { - RepositoryModel r = list.get(i); - - String lm = formatDate(r.lastChange); - String size = r.size; - if (!r.hasCommits) { - lm = ""; - size = FlipTable.EMPTY; - } - if (verbose) { - String owners = ""; - if (!ArrayUtils.isEmpty(r.owners)) { - owners = Joiner.on(",").join(r.owners); - } - data[i] = new Object[] { r.name, r.description, owners, lm, size }; - } else { - data[i] = new Object[] { r.name, lm, size }; - } - } - stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - } - - @Override - protected void asTabbed(List list) { - if (verbose) { - for (RepositoryModel r : list) { - String lm = formatDate(r.lastChange); - String owners = ""; - if (!ArrayUtils.isEmpty(r.owners)) { - owners = Joiner.on(",").join(r.owners); - } - String size = r.size; - if (!r.hasCommits) { - lm = ""; - size = "(empty)"; - } - - outTabbed(r.name, r.description == null ? "" : r.description, - owners, lm, size); - } - } else { - for (RepositoryModel r : list) { - outTabbed(r.name); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java deleted file mode 100644 index b3691cbb..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/ReviewCommand.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.util.HashSet; -import java.util.Set; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import com.gitblit.models.TicketModel.Change; -import com.gitblit.models.TicketModel.Patchset; -import com.gitblit.models.TicketModel.Score; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.gitblit.wicket.GitBlitWebSession; - -@CommandMetaData(name = "review", description = "Verify, approve and/or submit one or more patch sets", hidden = true) -public class ReviewCommand extends SshCommand { - - private final static short REV_ID_LEN = 40; - private final Set patchSets = new HashSet(); - - @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review") - void addPatchSetId(final String token) { - try { - patchSets.add(parsePatchSet(token)); - } catch (UnloggedFailure e) { - throw new IllegalArgumentException(e.getMessage(), e); - } - } - - @Option(name = "--project", required = true, aliases = "-p", usage = "project containing the specified patch set(s)") - private String project; - - @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change(s)", metaVar = "MESSAGE") - private String changeComment; - - @Option(name = "--vote", aliases = "-v", usage = "vote on this patch set", metaVar = "VOTE") - private int vote; - - @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)") - private boolean submitChange; - - @Override - public void run() throws UnloggedFailure { - UserModel user = GitBlitWebSession.get().getUser(); - // TODO ensure user has permission to score +2/-2 - for (Patchset ps : patchSets) { - // review - Change change = new Change(user.username); - change.review(ps, Score.fromScore(vote), false); - // TODO(davido): add patchset comment - if (submitChange) { - // TODO(davido): merge (when desired and the change is mergeable) - } - } - } - - private Patchset parsePatchSet(String ps) throws UnloggedFailure { - // By commit? - // - if (ps.matches("^([0-9a-fA-F]{4," + REV_ID_LEN + "})$")) { - // TODO; parse - } - - // By older style change,patchset? - // - if (ps.matches("^[1-9][0-9]*,[1-9][0-9]*$")) { - // TODO: parse - } - - throw new UnloggedFailure(1, "fatal: Cannot parse patchset: " + ps); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/TeamsDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/TeamsDispatcher.java deleted file mode 100644 index d0ec58f0..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/TeamsDispatcher.java +++ /dev/null @@ -1,507 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.util.ArrayList; -import java.util.List; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import com.gitblit.Constants.AccessPermission; -import com.gitblit.GitBlitException; -import com.gitblit.manager.IGitblit; -import com.gitblit.models.RegistrantAccessPermission; -import com.gitblit.models.RepositoryModel; -import com.gitblit.models.TeamModel; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.ListFilterCommand; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.gitblit.transport.ssh.commands.UsageExample; -import com.gitblit.transport.ssh.commands.UsageExamples; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.FlipTable; -import com.gitblit.utils.FlipTable.Borders; -import com.gitblit.utils.StringUtils; -import com.google.common.base.Joiner; - -@CommandMetaData(name = "teams", description = "Team management commands", admin = true) -public class TeamsDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - // primary team commands - register(user, NewTeam.class); - register(user, RenameTeam.class); - register(user, RemoveTeam.class); - register(user, ShowTeam.class); - register(user, ListTeams.class); - - // team-specific commands - register(user, SetField.class); - register(user, Permissions.class); - register(user, Members.class); - } - - public static abstract class TeamCommand extends SshCommand { - @Argument(index = 0, required = true, metaVar = "TEAM", usage = "team name") - protected String teamname; - - protected TeamModel getTeam(boolean requireTeam) throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - TeamModel team = gitblit.getTeamModel(teamname); - if (requireTeam && team == null) { - throw new UnloggedFailure(1, String.format("Team %s does not exist!", teamname)); - } - return team; - } - } - - @CommandMetaData(name = "new", aliases = { "add" }, description = "Create a new team") - @UsageExample(syntax = "${cmd} contributors --canFork --canCreate") - public static class NewTeam extends TeamCommand { - - @Option(name = "--canAdmin", usage = "can administer the server") - protected boolean canAdmin; - - @Option(name = "--canFork", usage = "can fork repositories") - protected boolean canFork; - - @Option(name = "--canCreate", usage = "can create personal repositories") - protected boolean canCreate; - - @Override - public void run() throws UnloggedFailure { - - if (getTeam(false) != null) { - throw new UnloggedFailure(1, String.format("Team %s already exists!", teamname)); - } - - TeamModel team = new TeamModel(teamname); - team.canAdmin = canAdmin; - team.canFork = canFork; - team.canCreate = canCreate; - - IGitblit gitblit = getContext().getGitblit(); - try { - gitblit.addTeam(team); - stdout.println(String.format("%s created.", teamname)); - } catch (GitBlitException e) { - String msg = String.format("Failed to create %s!", teamname); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - } - - @CommandMetaData(name = "rename", aliases = { "mv" }, description = "Rename a team") - @UsageExample(syntax = "${cmd} contributors friends", description = "Rename the contributors team to the friends team") - public static class RenameTeam extends TeamCommand { - @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "the new team name") - protected String newTeamName; - - @Override - public void run() throws UnloggedFailure { - TeamModel team = getTeam(true); - IGitblit gitblit = getContext().getGitblit(); - if (null != gitblit.getTeamModel(newTeamName)) { - throw new UnloggedFailure(1, String.format("Team %s already exists!", newTeamName)); - } - - // set the new team name - team.name = newTeamName; - - try { - gitblit.reviseTeam(teamname, team); - stdout.println(String.format("Renamed team %s to %s.", teamname, newTeamName)); - } catch (GitBlitException e) { - String msg = String.format("Failed to rename team from %s to %s", teamname, newTeamName); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - } - - @CommandMetaData(name = "set", description = "Set the specified field of a team") - @UsageExample(syntax = "${cmd} contributors canFork true", description = "Allow the contributors team to fork repositories") - public static class SetField extends TeamCommand { - - @Argument(index = 1, required = true, metaVar = "FIELD", usage = "the field to update") - protected String fieldName; - - @Argument(index = 2, required = true, metaVar = "VALUE", usage = "the new value") - protected List fieldValues = new ArrayList(); - - protected enum Field { - mailingList, preReceive, postReceive, canAdmin, canFork, canCreate; - - static Field fromString(String name) { - for (Field field : values()) { - if (field.name().equalsIgnoreCase(name)) { - return field; - } - } - return null; - } - } - - @Override - protected String getUsageText() { - String fields = Joiner.on(", ").join(Field.values()); - StringBuilder sb = new StringBuilder(); - sb.append("Valid fields are:\n ").append(fields); - return sb.toString(); - } - - @Override - public void run() throws UnloggedFailure { - TeamModel team = getTeam(true); - - Field field = Field.fromString(fieldName); - if (field == null) { - throw new UnloggedFailure(1, String.format("Unknown field %s", fieldName)); - } - - String value = Joiner.on(" ").join(fieldValues); - IGitblit gitblit = getContext().getGitblit(); - - switch(field) { - case mailingList: - team.mailingLists.clear(); - team.mailingLists.addAll(fieldValues); - break; - case preReceive: - team.preReceiveScripts.clear(); - team.preReceiveScripts.addAll(fieldValues); - break; - case postReceive: - team.postReceiveScripts.clear(); - team.postReceiveScripts.addAll(fieldValues); - break; - case canAdmin: - team.canAdmin = toBool(value); - break; - case canFork: - team.canFork = toBool(value); - break; - case canCreate: - team.canCreate = toBool(value); - break; - default: - throw new UnloggedFailure(1, String.format("Field %s was not properly handled by the set command.", fieldName)); - } - - try { - gitblit.reviseTeam(teamname, team); - stdout.println(String.format("Set %s.%s = %s", teamname, fieldName, value)); - } catch (GitBlitException e) { - String msg = String.format("Failed to set %s.%s = %s", teamname, fieldName, value); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - - protected boolean toBool(String value) throws UnloggedFailure { - String v = value.toLowerCase(); - if (v.equals("t") - || v.equals("true") - || v.equals("yes") - || v.equals("on") - || v.equals("y") - || v.equals("1")) { - return true; - } else if (v.equals("f") - || v.equals("false") - || v.equals("no") - || v.equals("off") - || v.equals("n") - || v.equals("0")) { - return false; - } - throw new UnloggedFailure(1, String.format("Invalid boolean value %s", value)); - } - } - - @CommandMetaData(name = "permissions", aliases = { "perms" }, description = "Add or remove permissions from a team") - @UsageExample(syntax = "${cmd} contributors RW:alpha/repo.git RWC:alpha/repo2.git", description = "Add or set permissions for contributors") - public static class Permissions extends TeamCommand { - - @Argument(index = 1, multiValued = true, metaVar = "[PERMISSION:]REPOSITORY", usage = "a repository expression") - protected List permissions; - - @Option(name = "--remove", aliases = { "-r" }, metaVar = "REPOSITORY|ALL", usage = "remove a repository permission") - protected List removals; - - @Override - public void run() throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - TeamModel team = getTeam(true); - - boolean modified = false; - if (!ArrayUtils.isEmpty(removals)) { - if (removals.contains("ALL")) { - team.permissions.clear(); - } else { - for (String repo : removals) { - team.removeRepositoryPermission(repo); - log.info(String.format("Removing permission for %s from %s", repo, teamname)); - } - } - modified = true; - } - - if (!ArrayUtils.isEmpty(permissions)) { - for (String perm : permissions) { - String repo = AccessPermission.repositoryFromRole(perm); - if (StringUtils.findInvalidCharacter(repo) == null) { - // explicit permision, confirm repository - RepositoryModel r = gitblit.getRepositoryModel(repo); - if (r == null) { - throw new UnloggedFailure(1, String.format("Repository %s does not exist!", repo)); - } - } - AccessPermission ap = AccessPermission.permissionFromRole(perm); - team.setRepositoryPermission(repo, ap); - log.info(String.format("Setting %s:%s for %s", ap.name(), repo, teamname)); - } - modified = true; - } - - if (modified && gitblit.updateTeamModel(teamname, team)) { - // reload & display new permissions - team = gitblit.getTeamModel(teamname); - } - - showPermissions(team); - } - - protected void showPermissions(TeamModel team) { - List perms = team.getRepositoryPermissions(); - String[] pheaders = { "Repository", "Permission", "Type" }; - Object [][] pdata = new Object[perms.size()][]; - for (int i = 0; i < perms.size(); i++) { - RegistrantAccessPermission ap = perms.get(i); - pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType }; - } - stdout.println(FlipTable.of(pheaders, pdata, Borders.BODY_HCOLS)); - } - } - - @CommandMetaData(name = "members", aliases = { "users" }, description = "Add or remove team members") - @UsageExample(syntax = "${cmd} contributors RW:alpha/repo.git RWC:alpha/repo2.git", description = "Add or set permissions for contributors") - public static class Members extends TeamCommand { - - @Argument(index = 1, multiValued = true, metaVar = "USERNAME", usage = "a username") - protected List members; - - @Option(name = "--remove", aliases = { "-r" }, metaVar = "USERNAME|ALL", usage = "remove a team member") - protected List removals; - - @Override - public void run() throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - TeamModel team = getTeam(true); - - boolean canEditMemberships = gitblit.supportsTeamMembershipChanges(team); - if (!canEditMemberships) { - String msg = String.format("Team %s (%s) does not permit membership changes!", team.name, team.accountType); - throw new UnloggedFailure(1, msg); - } - - boolean modified = false; - if (!ArrayUtils.isEmpty(removals)) { - if (removals.contains("ALL")) { - team.users.clear(); - } else { - for (String member : removals) { - team.removeUser(member); - log.info(String.format("Removing member %s from %s", member, teamname)); - } - } - modified = true; - } - - if (!ArrayUtils.isEmpty(members)) { - for (String username : members) { - UserModel u = gitblit.getUserModel(username); - if (u == null) { - throw new UnloggedFailure(1, String.format("Unknown user %s", username)); - } - boolean canEditTeams = gitblit.supportsTeamMembershipChanges(u); - if (!canEditTeams) { - String msg = String.format("User %s (%s) does not allow team membership changes ", u.username, u.accountType); - throw new UnloggedFailure(1, msg); - } - team.addUser(username); - } - modified = true; - } - - if (modified && gitblit.updateTeamModel(teamname, team)) { - // reload & display new permissions - team = gitblit.getTeamModel(teamname); - } - - String[] headers = { "Username", "Display Name" }; - Object [][] data = new Object[team.users.size()][]; - int i = 0; - for (String username : team.users) { - UserModel u = gitblit.getUserModel(username); - data[i] = new Object[] { username, u.displayName }; - i++; - } - stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - } - } - - @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove a team") - @UsageExample(syntax = "${cmd} contributors", description = "Delete the contributors team") - public static class RemoveTeam extends TeamCommand { - - @Override - public void run() throws UnloggedFailure { - - TeamModel team = getTeam(true); - IGitblit gitblit = getContext().getGitblit(); - if (gitblit.deleteTeamModel(team)) { - stdout.println(String.format("%s has been deleted.", teamname)); - } else { - throw new UnloggedFailure(1, String.format("Failed to delete %s!", teamname)); - } - } - } - - @CommandMetaData(name = "show", description = "Show the details of a team") - @UsageExample(syntax = "${cmd} contributors", description = "Display the 'contributors' team") - public static class ShowTeam extends TeamCommand { - - @Override - public void run() throws UnloggedFailure { - - TeamModel t = getTeam(true); - - // fields - StringBuilder fb = new StringBuilder(); - fb.append("Mailing Lists : ").append(Joiner.on(", ").join(t.mailingLists)).append('\n'); - fb.append("Type : ").append(t.accountType).append('\n'); - fb.append("Can Admin : ").append(t.canAdmin ? "Y":"").append('\n'); - fb.append("Can Fork : ").append(t.canFork ? "Y":"").append('\n'); - fb.append("Can Create : ").append(t.canCreate ? "Y":"").append('\n'); - fb.append("Pre-Receive : ").append(Joiner.on(", ").join(t.preReceiveScripts)).append('\n'); - fb.append("Post-Receive : ").append(Joiner.on(", ").join(t.postReceiveScripts)).append('\n'); - String fields = fb.toString(); - - // members - String members; - if (t.users.size() == 0) { - members = FlipTable.EMPTY; - } else { - IGitblit gitblit = getContext().getGitblit(); - String[] headers = { "Username", "Display Name" }; - Object [][] data = new Object[t.users.size()][]; - int i = 0; - for (String username : t.users) { - UserModel u = gitblit.getUserModel(username); - data[i] = new Object[] { username, u == null ? null : u.displayName }; - i++; - } - members = FlipTable.of(headers, data, Borders.COLS); - } - - // permissions - List perms = t.getRepositoryPermissions(); - String permissions; - if (perms.isEmpty()) { - permissions = FlipTable.EMPTY; - } else { - String[] pheaders = { "Repository", "Permission", "Type" }; - Object [][] pdata = new Object[perms.size()][]; - for (int i = 0; i < perms.size(); i++) { - RegistrantAccessPermission ap = perms.get(i); - pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType }; - } - permissions = FlipTable.of(pheaders, pdata, Borders.COLS); - } - - // assemble team table - String [] headers = new String[] { t.name }; - String[][] data = new String[6][]; - data[0] = new String [] { "FIELDS" }; - data[1] = new String [] { fields }; - data[2] = new String [] { "MEMBERS" }; - data[3] = new String [] { members }; - data[4] = new String [] { "PERMISSIONS" }; - data[5] = new String [] { permissions }; - stdout.println(FlipTable.of(headers, data)); - } - } - - @CommandMetaData(name = "list", aliases= { "ls" }, description = "List teams") - @UsageExamples(examples = { - @UsageExample(syntax = "${cmd}", description = "List teams as a table"), - @UsageExample(syntax = "${cmd} j.*", description = "List all teams that start with 'j'"), - }) - public static class ListTeams extends ListFilterCommand { - - @Override - protected List getItems() { - IGitblit gitblit = getContext().getGitblit(); - List teams = gitblit.getAllTeams(); - return teams; - } - - @Override - protected boolean matches(String filter, TeamModel t) { - return t.name.matches(filter); - } - - @Override - protected void asTable(List list) { - String[] headers = { "Name", "Members", "Type", "Create?", "Fork?"}; - Object[][] data = new Object[list.size()][]; - for (int i = 0; i < list.size(); i++) { - TeamModel t = list.get(i); - data[i] = new Object[] { - (t.canAdmin ? "*" : " ") + t.name, - t.users.isEmpty() ? "" : t.users.size(), - t.accountType + (t.canAdmin ? ",admin":""), - (t.canAdmin || t.canCreate) ? "Y":"", - (t.canAdmin || t.canFork) ? "Y" : ""}; - } - stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - } - - @Override - protected void asTabbed(List teams) { - if (verbose) { - for (TeamModel t : teams) { - outTabbed( - t.name, - t.users.isEmpty() ? "" : t.users.size(), - t.accountType + (t.canAdmin ? ",admin":""), - (t.canAdmin || t.canCreate) ? "Y":"", - (t.canAdmin || t.canFork) ? "Y" : ""); - } - } else { - for (TeamModel u : teams) { - outTabbed((u.canAdmin ? "*" : " ") + u.name); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java deleted file mode 100644 index dd29b6ac..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/TicketsDispatcher.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.util.List; - -import org.kohsuke.args4j.Argument; - -import com.gitblit.manager.IGitblit; -import com.gitblit.models.RepositoryModel; -import com.gitblit.models.TicketModel.Status; -import com.gitblit.models.UserModel; -import com.gitblit.tickets.ITicketService; -import com.gitblit.tickets.QueryBuilder; -import com.gitblit.tickets.QueryResult; -import com.gitblit.tickets.TicketIndexer.Lucene; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.ListCommand; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.FlipTable; -import com.gitblit.utils.FlipTable.Borders; -import com.gitblit.utils.StringUtils; - -@CommandMetaData(name = "tickets", description = "Ticket commands", hidden = true) -public class TicketsDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - register(user, ReviewCommand.class); - register(user, ListTickets.class); - } - - /* List tickets */ - @CommandMetaData(name = "list", aliases = { "ls" }, description = "List tickets") - public static class ListTickets extends ListCommand { - - private final String ALL = "ALL"; - - @Argument(index = 0, metaVar = "ALL|REPOSITORY", usage = "the repository or ALL") - protected String repository; - - @Argument(index = 1, multiValued = true, metaVar="CONDITION", usage = "query condition") - protected List query; - - protected String userQuery; - - @Override - protected List getItems() throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - ITicketService tickets = gitblit.getTicketService(); - - QueryBuilder sb = new QueryBuilder(); - if (ArrayUtils.isEmpty(query)) { - sb.and(Lucene.status.matches(Status.New.toString())).or(Lucene.status.matches(Status.Open.toString())); - } else { - StringBuilder b = new StringBuilder(); - for (String q : query) { - b.append(q).append(' '); - } - b.setLength(b.length() - 1); - sb.and(b.toString()); - } - - QueryBuilder qb; - if (StringUtils.isEmpty(repository) || ALL.equalsIgnoreCase(repository)) { - qb = sb; - userQuery = sb.build(); - } else { - qb = new QueryBuilder(); - RepositoryModel r = gitblit.getRepositoryModel(repository); - if (r == null) { - throw new UnloggedFailure(1, String.format("%s is not a repository!", repository)); - } - qb.and(Lucene.rid.matches(r.getRID())); - qb.and(sb.toSubquery().toString()); - userQuery = sb.build(); - } - - String query = qb.build(); - List list = tickets.queryFor(query, 0, 0, null, true); - return list; - } - - @Override - protected void asTable(List list) { - boolean forRepo = !StringUtils.isEmpty(repository) && !ALL.equalsIgnoreCase(repository); - String[] headers; - if (verbose) { - if (forRepo) { - String[] h = { "ID", "Title", "Status", "Last Modified", "Votes", "Commits" }; - headers = h; - } else { - String[] h = { "Repository", "ID", "Title", "Status", "Last Modified", "Votes", "Commits" }; - headers = h; - } - } else { - if (forRepo) { - String[] h = { "ID", "Title", "Status", "Last Modifed" }; - headers = h; - } else { - String[] h = { "Repository", "ID", "Title", "Status", "Last Modified" }; - headers = h; - } - } - - Object[][] data = new Object[list.size()][]; - for (int i = 0; i < list.size(); i++) { - QueryResult q = list.get(i); - - if (verbose) { - if (forRepo) { - data[i] = new Object[] { q.number, q.title, q.status, formatDate(q.getDate()), q.votesCount, q.patchset == null ? "": q.patchset.commits }; - } else { - data[i] = new Object[] { q.repository, q.number, q.title, q.status, formatDate(q.getDate()), q.votesCount, q.patchset == null ? "": q.patchset.commits }; - } - } else { - if (forRepo) { - data[i] = new Object[] { q.number, q.title, q.status, formatDate(q.getDate()) }; - } else { - data[i] = new Object[] { q.repository, q.number, q.title, q.status, formatDate(q.getDate()) }; - } - } - } - stdout.print(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - stdout.println(" " + repository + ": " + userQuery); - stdout.println(); - } - - @Override - protected void asTabbed(List list) { - if (verbose) { - for (QueryResult q : list) { - outTabbed(q.repository, q.number, q.title, q.status.toString(), - formatDate(q.getDate())); - } - } else { - for (QueryResult q : list) { - outTabbed(q.repository, q.number, q.title); - } - } - } - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java b/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java deleted file mode 100644 index 1a6dee46..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/UsersDispatcher.java +++ /dev/null @@ -1,592 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import java.util.ArrayList; -import java.util.List; - -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; - -import com.gitblit.Constants.AccessPermission; -import com.gitblit.GitBlitException; -import com.gitblit.Keys; -import com.gitblit.manager.IGitblit; -import com.gitblit.models.RegistrantAccessPermission; -import com.gitblit.models.RepositoryModel; -import com.gitblit.models.UserModel; -import com.gitblit.transport.ssh.SshKey; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.DispatchCommand; -import com.gitblit.transport.ssh.commands.ListFilterCommand; -import com.gitblit.transport.ssh.commands.SshCommand; -import com.gitblit.transport.ssh.commands.UsageExample; -import com.gitblit.transport.ssh.commands.UsageExamples; -import com.gitblit.utils.ArrayUtils; -import com.gitblit.utils.FlipTable; -import com.gitblit.utils.FlipTable.Borders; -import com.gitblit.utils.StringUtils; -import com.google.common.base.Joiner; - -@CommandMetaData(name = "users", description = "User management commands", admin = true) -public class UsersDispatcher extends DispatchCommand { - - @Override - protected void setup(UserModel user) { - // primary user commands - register(user, NewUser.class); - register(user, RenameUser.class); - register(user, RemoveUser.class); - register(user, ShowUser.class); - register(user, ListUsers.class); - - // user-specific commands - register(user, SetField.class); - register(user, Permissions.class); - register(user, DisableUser.class); - register(user, EnableUser.class); - } - - public static abstract class UserCommand extends SshCommand { - @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "username") - protected String username; - - protected UserModel getUser(boolean requireUser) throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - UserModel user = gitblit.getUserModel(username); - if (requireUser && user == null) { - throw new UnloggedFailure(1, String.format("User %s does not exist!", username)); - } - return user; - } - } - - @CommandMetaData(name = "new", aliases = { "add" }, description = "Create a new user account") - @UsageExample(syntax = "${cmd} john 12345 --email john@smith.com --canFork --canCreate") - public static class NewUser extends UserCommand { - - @Argument(index = 1, required = true, metaVar = "PASSWORD", usage = "password") - protected String password; - - @Option(name = "--email", metaVar = "ADDRESS", usage = "email address") - protected String email; - - @Option(name = "--canAdmin", usage = "can administer the server") - protected boolean canAdmin; - - @Option(name = "--canFork", usage = "can fork repositories") - protected boolean canFork; - - @Option(name = "--canCreate", usage = "can create personal repositories") - protected boolean canCreate; - - @Option(name = "--disabled", usage = "create a disabled user account") - protected boolean disabled; - - @Override - public void run() throws UnloggedFailure { - - if (getUser(false) != null) { - throw new UnloggedFailure(1, String.format("User %s already exists!", username)); - } - - UserModel user = new UserModel(username); - user.password = password; - - if (email != null) { - user.emailAddress = email; - } - - user.canAdmin = canAdmin; - user.canFork = canFork; - user.canCreate = canCreate; - user.disabled = disabled; - - IGitblit gitblit = getContext().getGitblit(); - try { - gitblit.addUser(user); - stdout.println(String.format("%s created.", username)); - } catch (GitBlitException e) { - log.error("Failed to add " + username, e); - throw new UnloggedFailure(1, e.getMessage()); - } - } - } - - @CommandMetaData(name = "rename", aliases = { "mv" }, description = "Rename an account") - @UsageExample(syntax = "${cmd} john frank", description = "Rename the account from john to frank") - public static class RenameUser extends UserCommand { - @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "the new account name") - protected String newUserName; - - @Override - public void run() throws UnloggedFailure { - UserModel user = getUser(true); - IGitblit gitblit = getContext().getGitblit(); - if (null != gitblit.getTeamModel(newUserName)) { - throw new UnloggedFailure(1, String.format("Team %s already exists!", newUserName)); - } - - // set the new name - user.username = newUserName; - - try { - gitblit.reviseUser(username, user); - stdout.println(String.format("Renamed user %s to %s.", username, newUserName)); - } catch (GitBlitException e) { - String msg = String.format("Failed to rename user from %s to %s", username, newUserName); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - } - - @CommandMetaData(name = "set", description = "Set the specified field of an account") - @UsageExample(syntax = "${cmd} john name John Smith", description = "Set the display name to \"John Smith\" for john's account") - public static class SetField extends UserCommand { - - @Argument(index = 1, required = true, metaVar = "FIELD", usage = "the field to update") - protected String fieldName; - - @Argument(index = 2, required = true, metaVar = "VALUE", usage = "the new value") - protected List fieldValues = new ArrayList(); - - protected enum Field { - name, displayName, email, password, canAdmin, canFork, canCreate, disabled; - - static Field fromString(String name) { - for (Field field : values()) { - if (field.name().equalsIgnoreCase(name)) { - return field; - } - } - return null; - } - } - - @Override - protected String getUsageText() { - String fields = Joiner.on(", ").join(Field.values()); - StringBuilder sb = new StringBuilder(); - sb.append("Valid fields are:\n ").append(fields); - return sb.toString(); - } - - @Override - public void run() throws UnloggedFailure { - UserModel user = getUser(true); - - Field field = Field.fromString(fieldName); - if (field == null) { - throw new UnloggedFailure(1, String.format("Unknown field %s", fieldName)); - } - - String value = Joiner.on(" ").join(fieldValues).trim(); - IGitblit gitblit = getContext().getGitblit(); - - boolean editCredentials = gitblit.supportsCredentialChanges(user); - boolean editDisplayName = gitblit.supportsDisplayNameChanges(user); - boolean editEmailAddress = gitblit.supportsEmailAddressChanges(user); - - String m = String.format("Can not edit %s for %s (%s)", field, user.username, user.accountType); - - switch(field) { - case name: - case displayName: - if (!editDisplayName) { - throw new UnloggedFailure(1, m); - } - user.displayName = value; - break; - case email: - if (!editEmailAddress) { - throw new UnloggedFailure(1, m); - } - user.emailAddress = value; - break; - case password: - if (!editCredentials) { - throw new UnloggedFailure(1, m); - } - int minLength = gitblit.getSettings().getInteger(Keys.realm.minPasswordLength, 5); - if (minLength < 4) { - minLength = 4; - } - if (value.trim().length() < minLength) { - throw new UnloggedFailure(1, "Password is too short."); - } - - // Optionally store the password MD5 digest. - String type = gitblit.getSettings().getString(Keys.realm.passwordStorage, "md5"); - if (type.equalsIgnoreCase("md5")) { - // store MD5 digest of password - user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(value); - } else if (type.equalsIgnoreCase("combined-md5")) { - // store MD5 digest of username+password - user.password = StringUtils.COMBINED_MD5_TYPE + StringUtils.getMD5(username + value); - } else { - user.password = value; - } - - // reset the cookie - user.cookie = StringUtils.getSHA1(user.username + value); - break; - case canAdmin: - user.canAdmin = toBool(value); - break; - case canFork: - user.canFork = toBool(value); - break; - case canCreate: - user.canCreate = toBool(value); - break; - case disabled: - user.disabled = toBool(value); - break; - default: - throw new UnloggedFailure(1, String.format("Field %s was not properly handled by the set command.", fieldName)); - } - - try { - gitblit.reviseUser(username, user); - stdout.println(String.format("Set %s.%s = %s", username, fieldName, value)); - } catch (GitBlitException e) { - String msg = String.format("Failed to set %s.%s = %s", username, fieldName, value); - log.error(msg, e); - throw new UnloggedFailure(1, msg); - } - } - - protected boolean toBool(String value) throws UnloggedFailure { - String v = value.toLowerCase(); - if (v.equals("t") - || v.equals("true") - || v.equals("yes") - || v.equals("on") - || v.equals("y") - || v.equals("1")) { - return true; - } else if (v.equals("f") - || v.equals("false") - || v.equals("no") - || v.equals("off") - || v.equals("n") - || v.equals("0")) { - return false; - } - throw new UnloggedFailure(1, String.format("Invalid boolean value %s", value)); - } - } - - @CommandMetaData(name = "disable", description = "Prohibit an account from authenticating") - @UsageExample(syntax = "${cmd} john", description = "Prevent John from authenticating") - public static class DisableUser extends UserCommand { - - @Override - public void run() throws UnloggedFailure { - - UserModel user = getUser(true); - user.disabled = true; - - IGitblit gitblit = getContext().getGitblit(); - if (gitblit.updateUserModel(username, user)) { - stdout.println(String.format("%s is not allowed to authenticate.", username)); - } else { - throw new UnloggedFailure(1, String.format("Failed to disable %s!", username)); - } - } - } - - @CommandMetaData(name = "enable", description = "Allow an account to authenticate") - @UsageExample(syntax = "${cmd} john", description = "Allow John to authenticate") - public static class EnableUser extends UserCommand { - - @Override - public void run() throws UnloggedFailure { - - UserModel user = getUser(true); - user.disabled = false; - - IGitblit gitblit = getContext().getGitblit(); - if (gitblit.updateUserModel(username, user)) { - stdout.println(String.format("%s may now authenticate.", username)); - } else { - throw new UnloggedFailure(1, String.format("Failed to enable %s!", username)); - } - } - } - - @CommandMetaData(name = "permissions", aliases = { "perms" }, description = "Add or remove permissions from an account") - @UsageExample(syntax = "${cmd} john RW:alpha/repo.git RWC:alpha/repo2.git", description = "Add or set permissions for John") - public static class Permissions extends UserCommand { - - @Argument(index = 1, multiValued = true, metaVar = "[PERMISSION:]REPOSITORY", usage = "a repository expression") - protected List permissions; - - @Option(name = "--remove", aliases = { "-r" }, metaVar = "REPOSITORY|ALL", usage = "remove a repository permission") - protected List removals; - - @Override - public void run() throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - UserModel user = getUser(true); - - boolean modified = false; - if (!ArrayUtils.isEmpty(removals)) { - if (removals.contains("ALL")) { - user.permissions.clear(); - } else { - for (String repo : removals) { - user.removeRepositoryPermission(repo); - log.info(String.format("Removing permission for %s from %s", repo, username)); - } - } - modified = true; - } - - if (!ArrayUtils.isEmpty(permissions)) { - for (String perm : permissions) { - String repo = AccessPermission.repositoryFromRole(perm); - if (StringUtils.findInvalidCharacter(repo) == null) { - // explicit permision, confirm repository - RepositoryModel r = gitblit.getRepositoryModel(repo); - if (r == null) { - throw new UnloggedFailure(1, String.format("Repository %s does not exist!", repo)); - } - } - AccessPermission ap = AccessPermission.permissionFromRole(perm); - user.setRepositoryPermission(repo, ap); - log.info(String.format("Setting %s:%s for %s", ap.name(), repo, username)); - } - modified = true; - } - - if (modified && gitblit.updateUserModel(username, user)) { - // reload & display new permissions - user = gitblit.getUserModel(username); - } - - showPermissions(user); - } - - protected void showPermissions(UserModel user) { - List perms = user.getRepositoryPermissions(); - String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" }; - Object [][] pdata = new Object[perms.size()][]; - for (int i = 0; i < perms.size(); i++) { - RegistrantAccessPermission ap = perms.get(i); - pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" }; - } - stdout.println(FlipTable.of(pheaders, pdata, Borders.BODY_HCOLS)); - } - } - - @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove a user account") - @UsageExample(syntax = "${cmd} john", description = "Delete john's account") - public static class RemoveUser extends UserCommand { - - @Override - public void run() throws UnloggedFailure { - - UserModel user = getUser(true); - IGitblit gitblit = getContext().getGitblit(); - if (gitblit.deleteUserModel(user)) { - stdout.println(String.format("%s has been deleted.", username)); - } else { - throw new UnloggedFailure(1, String.format("Failed to delete %s!", username)); - } - } - } - - @CommandMetaData(name = "show", description = "Show the details of an account") - @UsageExample(syntax = "${cmd} john", description = "Display john's account") - public static class ShowUser extends UserCommand { - - @Override - public void run() throws UnloggedFailure { - - UserModel u = getUser(true); - - // fields - StringBuilder fb = new StringBuilder(); - fb.append("Email : ").append(u.emailAddress == null ? "": u.emailAddress).append('\n'); - fb.append("Type : ").append(u.accountType).append('\n'); - fb.append("Can Admin : ").append(u.canAdmin() ? "Y":"").append('\n'); - fb.append("Can Fork : ").append(u.canFork() ? "Y":"").append('\n'); - fb.append("Can Create : ").append(u.canCreate() ? "Y":"").append('\n'); - String fields = fb.toString(); - - // teams - String teams; - if (u.teams.size() == 0) { - teams = FlipTable.EMPTY; - } else { - teams = Joiner.on(", ").join(u.teams); - } - - // owned repositories - String ownedTable; - List owned = new ArrayList(); - for (RepositoryModel repository : getContext().getGitblit().getRepositoryModels(u)) { - if (repository.isOwner(u.username)) { - owned.add(repository); - } - } - if (owned.isEmpty()) { - ownedTable = FlipTable.EMPTY; - } else { - String [] theaders = new String [] { "Repository", "Description" }; - Object [][] tdata = new Object[owned.size()][]; - int i = 0; - for (RepositoryModel r : owned) { - tdata[i] = new Object [] { r.name, r.description }; - i++; - } - ownedTable = FlipTable.of(theaders, tdata, Borders.COLS); - } - - // permissions - List perms = u.getRepositoryPermissions(); - String permissions; - if (perms.isEmpty()) { - permissions = FlipTable.EMPTY; - } else { - String[] pheaders = { "Repository", "Permission", "Type", "Source", "Mutable" }; - Object [][] pdata = new Object[perms.size()][]; - for (int i = 0; i < perms.size(); i++) { - RegistrantAccessPermission ap = perms.get(i); - pdata[i] = new Object[] { ap.registrant, ap.permission, ap.permissionType, ap.source, ap.mutable ? "Y":"" }; - } - permissions = FlipTable.of(pheaders, pdata, Borders.COLS); - } - - // keys - String keyTable; - List keys = getContext().getGitblit().getPublicKeyManager().getKeys(u.username); - if (ArrayUtils.isEmpty(keys)) { - keyTable = FlipTable.EMPTY; - } else { - String[] headers = { "#", "Fingerprint", "Comment", "Type" }; - int len = keys == null ? 0 : keys.size(); - Object[][] data = new Object[len][]; - for (int i = 0; i < len; i++) { - // show 1-based index numbers with the fingerprint - // this is useful for comparing with "ssh-add -l" - SshKey k = keys.get(i); - data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), k.getAlgorithm() }; - } - keyTable = FlipTable.of(headers, data, Borders.COLS); - } - - // assemble user table - String userTitle = u.getDisplayName() + (u.username.equals(u.getDisplayName()) ? "" : (" (" + u.username + ")")); - if (u.disabled) { - userTitle += " [DISABLED]"; - } - String [] headers = new String[] { userTitle }; - String[][] data = new String[8][]; - data[0] = new String [] { "FIELDS" }; - data[1] = new String [] { fields }; - data[2] = new String [] { "TEAMS" }; - data[3] = new String [] { teams }; - data[4] = new String [] { "OWNED REPOSITORIES" }; - data[5] = new String [] { ownedTable }; - data[4] = new String [] { "PERMISSIONS" }; - data[5] = new String [] { permissions }; - data[6] = new String [] { "SSH PUBLIC KEYS" }; - data[7] = new String [] { keyTable }; - stdout.println(FlipTable.of(headers, data)); - } - } - - @CommandMetaData(name = "list", aliases= { "ls" }, description = "List accounts") - @UsageExamples(examples = { - @UsageExample(syntax = "${cmd}", description = "List accounts as a table"), - @UsageExample(syntax = "${cmd} j.*", description = "List all accounts that start with 'j'"), - }) - public static class ListUsers extends ListFilterCommand { - - @Override - protected List getItems() { - IGitblit gitblit = getContext().getGitblit(); - List users = gitblit.getAllUsers(); - return users; - } - - @Override - protected boolean matches(String filter, UserModel u) { - return u.username.matches(filter); - } - - @Override - protected void asTable(List list) { - String[] headers; - if (verbose) { - String[] h = { "Name", "Display name", "Email", "Type", "Teams", "Create?", "Fork?"}; - headers = h; - } else { - String[] h = { "Name", "Display name", "Email", "Type"}; - headers = h; - } - - Object[][] data = new Object[list.size()][]; - for (int i = 0; i < list.size(); i++) { - UserModel u = list.get(i); - - String name = (u.disabled ? "-" : ((u.canAdmin() ? "*" : " "))) + u.username; - if (verbose) { - data[i] = new Object[] { - name, - u.displayName, - u.emailAddress, - u.accountType + (u.canAdmin() ? ",admin":""), - u.teams.isEmpty() ? "" : u.teams.size(), - (u.canAdmin() || u.canCreate()) ? "Y":"", - (u.canAdmin() || u.canFork()) ? "Y" : ""}; - } else { - data[i] = new Object[] { - name, - u.displayName, - u.emailAddress, - u.accountType + (u.canAdmin() ? ",admin":"")}; - } - } - stdout.print(FlipTable.of(headers, data, Borders.BODY_HCOLS)); - stdout.println(" * = admin account, - = disabled account"); - stdout.println(); - } - - @Override - protected void asTabbed(List users) { - if (verbose) { - for (UserModel u : users) { - outTabbed( - u.disabled ? "-" : ((u.canAdmin() ? "*" : " ")) + u.username, - u.getDisplayName(), - u.emailAddress == null ? "" : u.emailAddress, - u.accountType + (u.canAdmin() ? ",admin":""), - u.teams.isEmpty() ? "" : u.teams.size(), - (u.canAdmin() || u.canCreate()) ? "Y":"", - (u.canAdmin() || u.canFork()) ? "Y" : ""); - } - } else { - for (UserModel u : users) { - outTabbed(u.disabled ? "-" : ((u.canAdmin() ? "*" : " ")) + u.username); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java b/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java deleted file mode 100644 index 384c6ce4..00000000 --- a/src/main/java/com/gitblit/transport/ssh/gitblit/VersionCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2014 gitblit.com. - * - * Licensed 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 com.gitblit.transport.ssh.gitblit; - -import com.gitblit.Constants; -import com.gitblit.transport.ssh.commands.CommandMetaData; -import com.gitblit.transport.ssh.commands.SshCommand; - -@CommandMetaData(name="version", description = "Display the Gitblit version") -public class VersionCommand extends SshCommand { - - @Override - public void run() { - stdout.println(Constants.getGitBlitVersion()); - } -} diff --git a/src/main/java/com/gitblit/transport/ssh/keys/BaseKeyCommand.java b/src/main/java/com/gitblit/transport/ssh/keys/BaseKeyCommand.java new file mode 100644 index 00000000..588770f4 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/keys/BaseKeyCommand.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.keys; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.List; + +import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.transport.ssh.SshKey; +import com.gitblit.transport.ssh.commands.SshCommand; +import com.google.common.base.Charsets; + +/** + * + * Base class for commands that read SSH keys from stdin or a parameter list. + * + */ +abstract class BaseKeyCommand extends SshCommand { + + protected List readKeys(List sshKeys) + throws UnsupportedEncodingException, IOException { + int idx = -1; + if (sshKeys.isEmpty() || (idx = sshKeys.indexOf("-")) >= 0) { + String sshKey = ""; + BufferedReader br = new BufferedReader(new InputStreamReader( + in, Charsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + sshKey += line + "\n"; + } + if (idx == -1) { + sshKeys.add(sshKey.trim()); + } else { + sshKeys.set(idx, sshKey.trim()); + } + } + return sshKeys; + } + + protected IPublicKeyManager getKeyManager() { + return getContext().getGitblit().getPublicKeyManager(); + } + + protected SshKey parseKey(String rawData) throws UnloggedFailure { + if (rawData.contains("PRIVATE")) { + throw new UnloggedFailure(1, "Please provide a PUBLIC key, not a PRIVATE key!"); + } + SshKey key = new SshKey(rawData); + return key; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java new file mode 100644 index 00000000..ad373060 --- /dev/null +++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java @@ -0,0 +1,252 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.transport.ssh.keys; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.UserModel; +import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.transport.ssh.SshKey; +import com.gitblit.transport.ssh.commands.CommandMetaData; +import com.gitblit.transport.ssh.commands.DispatchCommand; +import com.gitblit.transport.ssh.commands.SshCommand; +import com.gitblit.transport.ssh.commands.UsageExample; +import com.gitblit.utils.FlipTable; +import com.gitblit.utils.FlipTable.Borders; +import com.google.common.base.Joiner; + +/** + * The dispatcher and it's commands for SSH public key management. + * + * @author James Moger + * + */ +@CommandMetaData(name = "keys", description = "SSH public key management commands") +public class KeysDispatcher extends DispatchCommand { + + @Override + protected void setup(UserModel user) { + register(user, AddKey.class); + register(user, RemoveKey.class); + register(user, ListKeys.class); + register(user, WhichKey.class); + register(user, CommentKey.class); + } + + @CommandMetaData(name = "add", description = "Add an SSH public key to your account") + @UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd} -", description = "Upload your SSH public key and add it to your account") + public static class AddKey extends BaseKeyCommand { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + @Argument(metaVar = "", usage = "the key(s) to add") + private List addKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + String username = getContext().getClient().getUsername(); + List keys = readKeys(addKeys); + for (String key : keys) { + SshKey sshKey = parseKey(key); + getKeyManager().addKey(username, sshKey); + log.info("added SSH public key for {}", username); + } + } + } + + @CommandMetaData(name = "remove", aliases = { "rm" }, description = "Remove an SSH public key from your account") + @UsageExample(syntax = "${cmd} 2", description = "Remove the SSH key identified as #2 in `keys list`") + public static class RemoveKey extends BaseKeyCommand { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private final String ALL = "ALL"; + + @Argument(metaVar = "||ALL", usage = "the key to remove", required = true) + private List removeKeys = new ArrayList(); + + @Override + public void run() throws IOException, UnloggedFailure { + String username = getContext().getClient().getUsername(); + // remove a key that has been piped to the command + // or remove all keys + + List currentKeys = getKeyManager().getKeys(username); + if (currentKeys == null || currentKeys.isEmpty()) { + throw new UnloggedFailure(1, "There are no registered keys!"); + } + + List keys = readKeys(removeKeys); + if (keys.contains(ALL)) { + if (getKeyManager().removeAllKeys(username)) { + stdout.println("Removed all keys."); + log.info("removed all SSH public keys from {}", username); + } else { + log.warn("failed to remove all SSH public keys from {}", username); + } + } else { + for (String key : keys) { + try { + // remove a key by it's index (1-based indexing) + int index = Integer.parseInt(key); + if (index > keys.size()) { + if (keys.size() == 1) { + throw new UnloggedFailure(1, "Invalid index specified. There is only 1 registered key."); + } + throw new UnloggedFailure(1, String.format("Invalid index specified. There are %d registered keys.", keys.size())); + } + SshKey sshKey = currentKeys.get(index - 1); + if (getKeyManager().removeKey(username, sshKey)) { + stdout.println(String.format("Removed %s", sshKey.getFingerprint())); + } else { + throw new UnloggedFailure(1, String.format("failed to remove #%s: %s", key, sshKey.getFingerprint())); + } + } catch (Exception e) { + // remove key by raw key data + SshKey sshKey = parseKey(key); + if (getKeyManager().removeKey(username, sshKey)) { + stdout.println(String.format("Removed %s", sshKey.getFingerprint())); + log.info("removed SSH public key {} from {}", sshKey.getFingerprint(), username); + } else { + log.warn("failed to remove SSH public key {} from {}", sshKey.getFingerprint(), username); + throw new UnloggedFailure(1, String.format("failed to remove %s", sshKey.getFingerprint())); + } + } + } + } + } + } + + @CommandMetaData(name = "list", aliases = { "ls" }, description = "List your registered SSH public keys") + public static class ListKeys extends SshCommand { + + @Option(name = "-L", usage = "list complete public key parameters") + private boolean showRaw; + + @Override + public void run() { + IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager(); + String username = getContext().getClient().getUsername(); + List keys = keyManager.getKeys(username); + + if (showRaw) { + asRaw(keys); + } else { + asTable(keys); + } + } + + /* output in the same format as authorized_keys */ + protected void asRaw(List keys) { + if (keys == null) { + return; + } + for (SshKey key : keys) { + stdout.println(key.getRawData()); + } + } + + protected void asTable(List keys) { + String[] headers = { "#", "Fingerprint", "Comment", "Type" }; + int len = keys == null ? 0 : keys.size(); + Object[][] data = new Object[len][]; + for (int i = 0; i < len; i++) { + // show 1-based index numbers with the fingerprint + // this is useful for comparing with "ssh-add -l" + SshKey k = keys.get(i); + data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), k.getAlgorithm() }; + } + + stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); + } + } + + @CommandMetaData(name = "which", description = "Display the SSH public key used for this session") + public static class WhichKey extends SshCommand { + + @Option(name = "-L", usage = "list complete public key parameters") + private boolean showRaw; + + @Override + public void run() throws UnloggedFailure { + SshKey key = getContext().getClient().getKey(); + if (key == null) { + throw new UnloggedFailure(1, "You have not authenticated with an SSH public key."); + } + + if (showRaw) { + stdout.println(key.getRawData()); + } else { + final String username = getContext().getClient().getUsername(); + List keys = getContext().getGitblit().getPublicKeyManager().getKeys(username); + int index = 0; + for (int i = 0; i < keys.size(); i++) { + if (key.equals(keys.get(i))) { + index = i + 1; + break; + } + } + asTable(index, key); + } + } + + protected void asTable(int index, SshKey key) { + String[] headers = { "#", "Fingerprint", "Comment", "Type" }; + Object[][] data = new Object[1][]; + data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getAlgorithm() }; + + stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); + } + } + + @CommandMetaData(name = "comment", description = "Set the comment for an SSH public key") + @UsageExample(syntax = "${cmd} 3 Home workstation", description = "Set the comment for key #3") + public static class CommentKey extends SshCommand { + + @Argument(index = 0, metaVar = "INDEX", usage = "the key index", required = true) + private int index; + + @Argument(index = 1, metaVar = "COMMENT", usage = "the new comment", required = true) + private List values = new ArrayList(); + + @Override + public void run() throws UnloggedFailure { + final String username = getContext().getClient().getUsername(); + IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager(); + List keys = keyManager.getKeys(username); + if (index > keys.size()) { + throw new UnloggedFailure(1, "Invalid key index!"); + } + + String comment = Joiner.on(" ").join(values); + SshKey key = keys.get(index - 1); + key.setComment(comment); + if (keyManager.addKey(username, key)) { + stdout.println(String.format("Updated the comment for key #%d.", index)); + } else { + throw new UnloggedFailure(1, String.format("Failed to update the comment for key #%d!", index)); + } + } + + } +} diff --git a/src/site/setup_transport_ssh.mkd b/src/site/setup_transport_ssh.mkd index 0f09910e..a671e5af 100644 --- a/src/site/setup_transport_ssh.mkd +++ b/src/site/setup_transport_ssh.mkd @@ -23,8 +23,8 @@ First you'll need to create an SSH key pair, if you don't already have one or if Then you can upload your *public* key right from the command-line. - cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 gitblit keys add - cat c:\\.ssh\id_rsa.pub | ssh -l -p 29418 gitblit keys add + cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 keys add + cat c:\\.ssh\id_rsa.pub | ssh -l -p 29418 keys add **NOTE:** It is important to note that *ssh-keygen* generates a public/private keypair (e.g. id_rsa and id_rsa.pub). You want to upload the *public* key, which is denoted by the *.pub* file extension. @@ -36,7 +36,7 @@ Once you've done both of those steps you should be able to execute the following Typing the following command syntax all the time gets to be rather tedious. - ssh -l -p 29418 gitblit version + ssh -l -p 29418 You can define an alias for your server which will reduce your command syntax to something like this. @@ -54,29 +54,33 @@ Create or modify your `~/.ssh/config` file and add a host entry. If you are on Gitblit supports SSH command plugins and provides several commands out-of-the-box. -#### gitblit +#### keys -The *gitblit* command has many subcommands for interacting with Gitblit. +The *keys* command dispatcher allows you to manage your public ssh keys. You can list keys, add keys, remove keys, and identify the key in-use for the active session. ##### keys add Add an SSH public key to your account. This command accepts a public key piped to stdin. - cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 gitblit keys add + cat ~/.ssh/id_rsa.pub | ssh -l -p 29418 keys add ##### keys list Show the SSH public keys you have added to your account. - ssh -l -p 29418 gitblit keys list + ssh -l -p 29418 keys list ##### keys remove Remove an SSH public key from your account. This command accepts several input values, the most useful one is an index number which matches the index number displayed in the `list` command. - ssh -l -p 29418 gitblit keys remove 2 + ssh -l -p 29418 keys remove 2 You can also remove all your public keys from your account. - ssh -l -p 29418 gitblit keys remove ALL + ssh -l -p 29418 keys remove ALL + +### SSH Command Plugins + +Gitblit supports loading custom SSH command plugins. diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java index dbd1d868..620190ef 100644 --- a/src/test/java/com/gitblit/tests/SshDaemonTest.java +++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java @@ -93,7 +93,7 @@ public class SshDaemonTest extends GitblitUnitTest { pair.getPublic().getEncoded(); assertTrue(session.authPublicKey("admin", pair).await().isSuccess()); - ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, "gitblit version"); + ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, "version"); ByteArrayOutputStream baos = new ByteArrayOutputStream(); Writer w = new OutputStreamWriter(baos); w.close(); -- cgit v1.2.3 From ec3b719dab4ae3c51d8bd52ab2b4176f82c5006f Mon Sep 17 00:00:00 2001 From: James Moger Date: Sun, 30 Mar 2014 15:26:54 -0400 Subject: Documentation --- src/site/resources/6x12.dfont | Bin 0 -> 89952 bytes src/site/resources/6x13.dfont | Bin 0 -> 132838 bytes src/site/resources/7x13.dfont | Bin 0 -> 120394 bytes src/site/resources/7x14.dfont | Bin 0 -> 89640 bytes src/site/setup_plugins.mkd | 43 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/site/resources/6x12.dfont create mode 100644 src/site/resources/6x13.dfont create mode 100644 src/site/resources/7x13.dfont create mode 100644 src/site/resources/7x14.dfont (limited to 'src') diff --git a/src/site/resources/6x12.dfont b/src/site/resources/6x12.dfont new file mode 100644 index 00000000..8c35f35a Binary files /dev/null and b/src/site/resources/6x12.dfont differ diff --git a/src/site/resources/6x13.dfont b/src/site/resources/6x13.dfont new file mode 100644 index 00000000..6cf59fe9 Binary files /dev/null and b/src/site/resources/6x13.dfont differ diff --git a/src/site/resources/7x13.dfont b/src/site/resources/7x13.dfont new file mode 100644 index 00000000..577d638b Binary files /dev/null and b/src/site/resources/7x13.dfont differ diff --git a/src/site/resources/7x14.dfont b/src/site/resources/7x14.dfont new file mode 100644 index 00000000..465af8ea Binary files /dev/null and b/src/site/resources/7x14.dfont differ diff --git a/src/site/setup_plugins.mkd b/src/site/setup_plugins.mkd index 4e5c733a..6ab4f1ef 100644 --- a/src/site/setup_plugins.mkd +++ b/src/site/setup_plugins.mkd @@ -3,4 +3,45 @@ *SINCE 1.5.0* -Document (limited) plugin infrastructure. +Gitblit supports extending and enhacing the core functionality through plugins. This mechanism is very young and incomplete with few extension points, but you can expect it to evolve rapidly in upcoming releases. + +### Architecture + +The existing plugin mechanism is based on [pf4j](https://github.com/decebals/pf4j). Plugins are distributed as zip files and may include their runtime dependencies or may rely on the bundled dependencies of other plugins and/or Gitblit core. + +The zip plugins are stored in `${baseFolder}/plugins` and are unpacked on startup into folders of the same name. + +A plugin defines it's metadata in the META-INF/MANIFEST.MF file: + + Plugin-Class: com.gitblit.plugins.cookbook.CookbookPlugin + Plugin-Dependencies: foo, bar + Plugin-Id: gitblit-plugin + Plugin-Provider: John Doe + Plugin-Version: 1.0 + +In addition to extending Gitblit core, plugins can also define extension points that may be implemented by other plugins. Therefore a plugin may depend on other plugins. + + Plugin-Dependencies: foo, bar + +Plugins are controlled by the `plugin` SSH dispatch command. Only *administrators* have permission to use this dispatch command. + +The pf4j plugin framework relies on a javac apt processor to generate compile-time extension information, so be sure to enable apt processing in your build process. + +### Extension Point: SSH DispatchCommand + +You can provide your own custom SSH commands by extending the DispatchCommand. + +For some examples of how to do this, please see: + +[gitblit-cookbook-plugin (Maven project)](https://dev.gitblit.com/summary/gitblit-cookbook-plugin.git) +[gitblit-powertools-plugin (Ant/Moxie project)](https://dev.gitblit.com/summary/gitblit-powertools-plugin.git) + +### Mac OSX Fonts + +Gitblit's core SSH commands and those in the *powertools* plugin rely on use of ANSI border characters to provide a pretty presentation of data. Unfortunately, the fonts provided by Apple - while very nice - don't work well with ANSI border characters. The following public domain fixed-width, fixed-point, bitmapped fonts work very nicely. I find the 6x12 font with a line spacing of ~0.8 to be quite acceptable. + +[6x12.dfont](6x12.dfont) +[6x13.dfont](6x13.dfont) +[7x13.dfont](7x13.dfont) +[7x14.dfont](7x14.dfont) + -- cgit v1.2.3 From e5d0bacbf746e09a9194822b231cb27090f58973 Mon Sep 17 00:00:00 2001 From: James Moger Date: Tue, 1 Apr 2014 00:10:43 -0400 Subject: Implement simple JSON-based plugin registry and install command --- releases.moxie | 1 + src/main/distrib/data/gitblit.properties | 20 +- .../java/com/gitblit/manager/GitblitManager.java | 36 +++ .../java/com/gitblit/manager/IPluginManager.java | 48 +++- .../java/com/gitblit/manager/PluginManager.java | 250 ++++++++++++++++++++- .../java/com/gitblit/models/PluginRegistry.java | 143 ++++++++++++ .../transport/ssh/commands/PluginDispatcher.java | 120 ++++++++-- 7 files changed, 588 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/gitblit/models/PluginRegistry.java (limited to 'src') diff --git a/releases.moxie b/releases.moxie index 0f37bf2b..89a7a5f3 100644 --- a/releases.moxie +++ b/releases.moxie @@ -48,6 +48,7 @@ r22: { - { name: 'git.sshBackend', defaultValue: 'NIO2' } - { name: 'git.sshCommandStartThreads', defaultValue: '2' } - { name: 'plugins.folder', defaultValue: '${baseFolder}/plugins' } + - { name: 'plugins.registry', defaultValue: 'http://gitblit.github.io/gitblit-registry/plugins.json' } } # diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 1a613e25..c52423b9 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -548,6 +548,18 @@ tickets.redis.url = # SINCE 1.4.0 tickets.perPage = 25 +# The folder where plugins are loaded from. +# +# SINCE 1.5.0 +# RESTART REQUIRED +# BASEFOLDER +plugins.folder = ${baseFolder}/plugins + +# The registry of available plugins. +# +# SINCE 1.5.0 +plugins.registry = http://gitblit.github.io/gitblit-registry/plugins.json + # # Groovy Integration # @@ -1850,11 +1862,3 @@ server.requireClientCertificates = false # SINCE 0.5.0 # RESTART REQUIRED server.shutdownPort = 8081 - -# Base folder for plugins. -# This folder may contain Gitblit plugins -# -# SINCE 1.6.0 -# RESTART REQUIRED -# BASEFOLDER -plugins.folder = ${baseFolder}/plugins diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index 6b1cc8a5..5a7d15ae 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -61,6 +61,8 @@ import com.gitblit.models.ForkModel; import com.gitblit.models.GitClientApplication; import com.gitblit.models.Mailing; import com.gitblit.models.Metric; +import com.gitblit.models.PluginRegistry.PluginRegistration; +import com.gitblit.models.PluginRegistry.PluginRelease; import com.gitblit.models.ProjectModel; import com.gitblit.models.RegistrantAccessPermission; import com.gitblit.models.RepositoryModel; @@ -1180,6 +1182,10 @@ public class GitblitManager implements IGitblit { return repositoryManager.isIdle(repository); } + /* + * PLUGIN MANAGER + */ + @Override public List getExtensions(Class clazz) { return pluginManager.getExtensions(clazz); @@ -1195,6 +1201,36 @@ public class GitblitManager implements IGitblit { return pluginManager.deletePlugin(wrapper); } + @Override + public boolean refreshRegistry() { + return pluginManager.refreshRegistry(); + } + + @Override + public boolean installPlugin(String url) { + return pluginManager.installPlugin(url); + } + + @Override + public boolean installPlugin(PluginRelease pv) { + return pluginManager.installPlugin(pv); + } + + @Override + public List getRegisteredPlugins() { + return pluginManager.getRegisteredPlugins(); + } + + @Override + public PluginRegistration lookupPlugin(String idOrName) { + return pluginManager.lookupPlugin(idOrName); + } + + @Override + public PluginRelease lookupRelease(String idOrName, String version) { + return pluginManager.lookupRelease(idOrName, version); + } + @Override public List getPlugins() { return pluginManager.getPlugins(); diff --git a/src/main/java/com/gitblit/manager/IPluginManager.java b/src/main/java/com/gitblit/manager/IPluginManager.java index 11b81ea3..1f7f85ee 100644 --- a/src/main/java/com/gitblit/manager/IPluginManager.java +++ b/src/main/java/com/gitblit/manager/IPluginManager.java @@ -15,9 +15,14 @@ */ package com.gitblit.manager; +import java.util.List; + import ro.fortsoft.pf4j.PluginManager; import ro.fortsoft.pf4j.PluginWrapper; +import com.gitblit.models.PluginRegistry.PluginRegistration; +import com.gitblit.models.PluginRegistry.PluginRelease; + public interface IPluginManager extends IManager, PluginManager { /** @@ -27,12 +32,51 @@ public interface IPluginManager extends IManager, PluginManager { * @return PluginWrapper that loaded the given class */ PluginWrapper whichPlugin(Class clazz); - + /** * Delete the plugin represented by {@link PluginWrapper}. - * + * * @param wrapper * @return true if successful */ boolean deletePlugin(PluginWrapper wrapper); + + /** + * Refresh the plugin registry. + */ + boolean refreshRegistry(); + + /** + * Install the plugin from the specified url. + */ + boolean installPlugin(String url); + + /** + * Install the plugin. + */ + boolean installPlugin(PluginRelease pr); + + /** + * The list of all registered plugins. + * + * @return a list of registered plugins + */ + List getRegisteredPlugins(); + + /** + * Lookup a plugin registration from the plugin registries. + * + * @param idOrName + * @return a plugin registration or null + */ + PluginRegistration lookupPlugin(String idOrName); + + /** + * Lookup a plugin release. + * + * @param idOrName + * @param version (use null for the current version) + * @return the identified plugin version or null + */ + PluginRelease lookupRelease(String idOrName, String version); } diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index e23aaec0..7b03f50d 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -15,31 +15,57 @@ */ package com.gitblit.manager; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.fortsoft.pf4j.DefaultPluginManager; +import ro.fortsoft.pf4j.PluginVersion; import ro.fortsoft.pf4j.PluginWrapper; import com.gitblit.Keys; +import com.gitblit.models.PluginRegistry; +import com.gitblit.models.PluginRegistry.PluginRegistration; +import com.gitblit.models.PluginRegistry.PluginRelease; +import com.gitblit.utils.Base64; import com.gitblit.utils.FileUtils; +import com.gitblit.utils.JsonUtils; +import com.gitblit.utils.StringUtils; +import com.google.common.io.Files; +import com.google.common.io.InputSupplier; /** * The plugin manager maintains the lifecycle of plugins. It is exposed as * Dagger bean. The extension consumers supposed to retrieve plugin manager * from the Dagger DI and retrieve extensions provided by active plugins. - * + * * @author David Ostrovsky - * + * */ public class PluginManager extends DefaultPluginManager implements IPluginManager { private final Logger logger = LoggerFactory.getLogger(getClass()); - + private final IRuntimeManager runtimeManager; + // timeout defaults of Maven 3.0.4 in seconds + private int connectTimeout = 20; + + private int readTimeout = 12800; + public PluginManager(IRuntimeManager runtimeManager) { super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins")); this.runtimeManager = runtimeManager; @@ -60,13 +86,13 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage stopPlugins(); return null; } - + @Override public boolean deletePlugin(PluginWrapper pw) { File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); File pluginFolder = new File(folder, pw.getPluginPath()); File pluginZip = new File(folder, pw.getPluginPath() + ".zip"); - + if (pluginFolder.exists()) { FileUtils.delete(pluginFolder); } @@ -75,4 +101,218 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } return true; } + + @Override + public boolean refreshRegistry() { + String dr = "http://gitblit.github.io/gitblit-registry/plugins.json"; + String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr); + try { + return download(url); + } catch (Exception e) { + logger.error(String.format("Failed to retrieve plugins.json from %s", url), e); + } + return false; + } + + protected List getRegistries() { + List list = new ArrayList(); + File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + FileFilter jsonFilter = new FileFilter() { + @Override + public boolean accept(File file) { + return !file.isDirectory() && file.getName().toLowerCase().endsWith(".json"); + } + }; + + File [] files = folder.listFiles(jsonFilter); + if (files == null || files.length == 0) { + // automatically retrieve the registry if we don't have a local copy + refreshRegistry(); + files = folder.listFiles(jsonFilter); + } + + if (files == null || files.length == 0) { + return list; + } + + for (File file : files) { + PluginRegistry registry = null; + try { + String json = FileUtils.readContent(file, "\n"); + registry = JsonUtils.fromJsonString(json, PluginRegistry.class); + } catch (Exception e) { + logger.error("Failed to deserialize " + file, e); + } + if (registry != null) { + list.add(registry); + } + } + return list; + } + + @Override + public List getRegisteredPlugins() { + List list = new ArrayList(); + Map map = new TreeMap(); + for (PluginRegistry registry : getRegistries()) { + List registrations = registry.registrations; + list.addAll(registrations); + for (PluginRegistration reg : registrations) { + reg.installedRelease = null; + map.put(reg.id, reg); + } + } + for (PluginWrapper pw : getPlugins()) { + String id = pw.getDescriptor().getPluginId(); + PluginVersion pv = pw.getDescriptor().getVersion(); + PluginRegistration reg = map.get(id); + if (reg != null) { + reg.installedRelease = pv.toString(); + } + } + return list; + } + + @Override + public PluginRegistration lookupPlugin(String idOrName) { + for (PluginRegistry registry : getRegistries()) { + PluginRegistration reg = registry.lookup(idOrName); + if (reg != null) { + return reg; + } + } + return null; + } + + @Override + public PluginRelease lookupRelease(String idOrName, String version) { + for (PluginRegistry registry : getRegistries()) { + PluginRegistration reg = registry.lookup(idOrName); + if (reg != null) { + PluginRelease pv; + if (StringUtils.isEmpty(version)) { + pv = reg.getCurrentRelease(); + } else { + pv = reg.getRelease(version); + } + if (pv != null) { + return pv; + } + } + } + return null; + } + + + /** + * Installs the plugin from the plugin version. + * + * @param pv + * @throws IOException + * @return true if successful + */ + @Override + public boolean installPlugin(PluginRelease pv) { + return installPlugin(pv.url); + } + + /** + * Installs the plugin from the url. + * + * @param url + * @return true if successful + */ + @Override + public boolean installPlugin(String url) { + try { + if (!download(url)) { + return false; + } + // TODO stop, unload, load + } catch (IOException e) { + logger.error("Failed to install plugin from " + url, e); + } + return true; + } + + /** + * Download a file to the plugins folder. + * + * @param url + * @return + * @throws IOException + */ + protected boolean download(String url) throws IOException { + File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp"); + if (tmpFile.exists()) { + tmpFile.delete(); + } + + URL u = new URL(url); + final URLConnection conn = getConnection(u); + + // try to get the server-specified last-modified date of this artifact + long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis()); + + Files.copy(new InputSupplier() { + @Override + public InputStream getInput() throws IOException { + return new BufferedInputStream(conn.getInputStream()); + } + }, tmpFile); + + File destFile = new File(pFolder, StringUtils.getLastPathElement(u.getPath())); + if (destFile.exists()) { + destFile.delete(); + } + tmpFile.renameTo(destFile); + destFile.setLastModified(lastModified); + + return true; + } + + protected URLConnection getConnection(URL url) throws IOException { + java.net.Proxy proxy = getProxy(url); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy); + if (java.net.Proxy.Type.DIRECT != proxy.type()) { + String auth = getProxyAuthorization(url); + conn.setRequestProperty("Proxy-Authorization", auth); + } + + String username = null; + String password = null; + if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { + // set basic authentication header + String auth = Base64.encodeBytes((username + ":" + password).getBytes()); + conn.setRequestProperty("Authorization", "Basic " + auth); + } + + // configure timeouts + conn.setConnectTimeout(connectTimeout * 1000); + conn.setReadTimeout(readTimeout * 1000); + + switch (conn.getResponseCode()) { + case HttpURLConnection.HTTP_MOVED_TEMP: + case HttpURLConnection.HTTP_MOVED_PERM: + // handle redirects by closing this connection and opening a new + // one to the new location of the requested resource + String newLocation = conn.getHeaderField("Location"); + if (!StringUtils.isEmpty(newLocation)) { + logger.info("following redirect to {0}", newLocation); + conn.disconnect(); + return getConnection(new URL(newLocation)); + } + } + + return conn; + } + + protected Proxy getProxy(URL url) { + return java.net.Proxy.NO_PROXY; + } + + protected String getProxyAuthorization(URL url) { + return ""; + } } diff --git a/src/main/java/com/gitblit/models/PluginRegistry.java b/src/main/java/com/gitblit/models/PluginRegistry.java new file mode 100644 index 00000000..c81a0f23 --- /dev/null +++ b/src/main/java/com/gitblit/models/PluginRegistry.java @@ -0,0 +1,143 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.models; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.parboiled.common.StringUtils; + +import ro.fortsoft.pf4j.PluginVersion; + +/** + * Represents a list of plugin registrations. + */ +public class PluginRegistry implements Serializable { + + private static final long serialVersionUID = 1L; + + public final String name; + + public final List registrations; + + public PluginRegistry(String name) { + this.name = name; + registrations = new ArrayList(); + } + + public PluginRegistration lookup(String idOrName) { + for (PluginRegistration registration : registrations) { + if (registration.id.equalsIgnoreCase(idOrName) + || registration.name.equalsIgnoreCase(idOrName)) { + return registration; + } + } + return null; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + public static enum InstallState { + NOT_INSTALLED, INSTALLED, CAN_UPDATE, UNKNOWN + } + + /** + * Represents a plugin registration. + */ + public static class PluginRegistration implements Serializable { + + private static final long serialVersionUID = 1L; + + public final String id; + + public String name; + + public String description; + + public String provider; + + public String projectUrl; + + public String currentRelease; + + public transient String installedRelease; + + public List releases; + + public PluginRegistration(String id) { + this.id = id; + this.releases = new ArrayList(); + } + + public PluginRelease getCurrentRelease() { + PluginRelease current = null; + if (!StringUtils.isEmpty(currentRelease)) { + current = getRelease(currentRelease); + } + + if (current == null) { + Date date = new Date(0); + for (PluginRelease pv : releases) { + if (pv.date.after(date)) { + current = pv; + } + } + } + return current; + } + + public PluginRelease getRelease(String version) { + for (PluginRelease pv : releases) { + if (pv.version.equalsIgnoreCase(version)) { + return pv; + } + } + return null; + } + + public InstallState getInstallState() { + if (StringUtils.isEmpty(installedRelease)) { + return InstallState.NOT_INSTALLED; + } + PluginVersion ir = PluginVersion.createVersion(installedRelease); + PluginVersion cr = PluginVersion.createVersion(currentRelease); + switch (ir.compareTo(cr)) { + case -1: + return InstallState.UNKNOWN; + case 1: + return InstallState.CAN_UPDATE; + default: + return InstallState.INSTALLED; + } + } + + @Override + public String toString() { + return id; + } + } + + public static class PluginRelease { + public String version; + public Date date; + public String url; + } +} diff --git a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java index 5c413db2..ba6f30d6 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; import ro.fortsoft.pf4j.PluginDependency; import ro.fortsoft.pf4j.PluginDescriptor; @@ -26,6 +27,8 @@ import ro.fortsoft.pf4j.PluginState; import ro.fortsoft.pf4j.PluginWrapper; import com.gitblit.manager.IGitblit; +import com.gitblit.models.PluginRegistry.PluginRegistration; +import com.gitblit.models.PluginRegistry.PluginRelease; import com.gitblit.models.UserModel; import com.gitblit.utils.FlipTable; import com.gitblit.utils.FlipTable.Borders; @@ -46,7 +49,8 @@ public class PluginDispatcher extends DispatchCommand { register(user, StopPlugin.class); register(user, ShowPlugin.class); register(user, RemovePlugin.class); - register(user, UploadPlugin.class); + register(user, InstallPlugin.class); + register(user, AvailablePlugins.class); } @CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins") @@ -82,7 +86,7 @@ public class PluginDispatcher extends DispatchCommand { stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); } - + @Override protected void asTabbed(List list) { for (PluginWrapper pw : list) { @@ -95,7 +99,7 @@ public class PluginDispatcher extends DispatchCommand { } } } - + @CommandMetaData(name = "start", description = "Start a plugin") public static class StartPlugin extends SshCommand { @@ -128,7 +132,7 @@ public class PluginDispatcher extends DispatchCommand { } } } - + protected void start(PluginWrapper pw) throws UnloggedFailure { String id = pw.getDescriptor().getPluginId(); if (pw.getPluginState() == PluginState.STARTED) { @@ -143,7 +147,7 @@ public class PluginDispatcher extends DispatchCommand { } } } - + @CommandMetaData(name = "stop", description = "Stop a plugin") public static class StopPlugin extends SshCommand { @@ -177,7 +181,7 @@ public class PluginDispatcher extends DispatchCommand { } } } - + protected void stop(PluginWrapper pw) throws UnloggedFailure { String id = pw.getDescriptor().getPluginId(); if (pw.getPluginState() == PluginState.STOPPED) { @@ -192,7 +196,7 @@ public class PluginDispatcher extends DispatchCommand { } } } - + @CommandMetaData(name = "show", description = "Show the details of a plugin") public static class ShowPlugin extends SshCommand { @@ -230,7 +234,7 @@ public class PluginDispatcher extends DispatchCommand { String ext = exts.get(i); data[0] = new Object[] { ext.toString(), ext.toString() }; } - extensions = FlipTable.of(headers, data, Borders.COLS); + extensions = FlipTable.of(headers, data, Borders.COLS); } // DEPENDENCIES @@ -246,9 +250,9 @@ public class PluginDispatcher extends DispatchCommand { PluginDependency dep = deps.get(i); data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() }; } - dependencies = FlipTable.of(headers, data, Borders.COLS); + dependencies = FlipTable.of(headers, data, Borders.COLS); } - + String[] headers = { d.getPluginId() }; Object[][] data = new Object[5][]; data[0] = new Object[] { fields }; @@ -256,10 +260,10 @@ public class PluginDispatcher extends DispatchCommand { data[2] = new Object[] { extensions }; data[3] = new Object[] { "DEPENDENCIES" }; data[4] = new Object[] { dependencies }; - stdout.println(FlipTable.of(headers, data)); + stdout.println(FlipTable.of(headers, data)); } } - + @CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true) public static class RemovePlugin extends SshCommand { @@ -282,12 +286,98 @@ public class PluginDispatcher extends DispatchCommand { } } } - - @CommandMetaData(name = "receive", aliases= { "upload" }, description = "Upload a plugin to the server", hidden = true) - public static class UploadPlugin extends SshCommand { + + @CommandMetaData(name = "install", description = "Download and installs a plugin", hidden = true) + public static class InstallPlugin extends SshCommand { + + @Argument(index = 0, required = true, metaVar = "||", usage = "the id, name, or the url of the plugin to download and install") + protected String urlOrIdOrName; + + @Option(name = "--version", usage = "The specific version to install") + private String version; @Override public void run() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + try { + String ulc = urlOrIdOrName.toLowerCase(); + if (ulc.startsWith("http://") || ulc.startsWith("https://")) { + if (gitblit.installPlugin(urlOrIdOrName)) { + stdout.println(String.format("Installed %s", urlOrIdOrName)); + } else { + new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName)); + } + } else { + PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version); + if (pv == null) { + throw new UnloggedFailure(1, String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName)); + } + if (gitblit.installPlugin(pv)) { + stdout.println(String.format("Installed %s", urlOrIdOrName)); + } else { + throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName)); + } + } + } catch (Exception e) { + log.error("Failed to install " + urlOrIdOrName, e); + throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName), e); + } + } + } + + @CommandMetaData(name = "available", description = "List the available plugins") + public static class AvailablePlugins extends ListFilterCommand { + + @Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry") + protected boolean refresh; + + @Override + protected List getItems() throws UnloggedFailure { + IGitblit gitblit = getContext().getGitblit(); + if (refresh) { + gitblit.refreshRegistry(); + } + List list = gitblit.getRegisteredPlugins(); + return list; + } + + @Override + protected boolean matches(String filter, PluginRegistration t) { + return t.id.matches(filter) || t.name.matches(filter); + } + + @Override + protected void asTable(List list) { + String[] headers; + if (verbose) { + String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" }; + headers = h; + } else { + String [] h = { "Name", "Description", "Installed", "Release", "State" }; + headers = h; + } + Object[][] data = new Object[list.size()][]; + for (int i = 0; i < list.size(); i++) { + PluginRegistration p = list.get(i); + if (verbose) { + data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider}; + } else { + data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()}; + } + } + + stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); + } + + @Override + protected void asTabbed(List list) { + for (PluginRegistration p : list) { + if (verbose) { + outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider); + } else { + outTabbed(p.name, p.description, p.currentRelease, p.getInstallState()); + } + } } } } -- cgit v1.2.3 From c78b25d102fe700617011a4c8acc0d35f9a9e6ca Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 2 Apr 2014 09:41:23 -0400 Subject: Support specifying permission levels for SSH public keys --- src/main/java/com/gitblit/Constants.java | 2 + .../com/gitblit/transport/ssh/FileKeyManager.java | 48 ++++++++++++++-------- .../java/com/gitblit/transport/ssh/SshKey.java | 47 +++++++++++++++++++++ .../com/gitblit/transport/ssh/git/Receive.java | 5 +++ .../java/com/gitblit/transport/ssh/git/Upload.java | 5 +++ .../gitblit/transport/ssh/keys/KeysDispatcher.java | 34 ++++++++++++--- 6 files changed, 120 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java index 56dfec06..26e0de3c 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java @@ -423,6 +423,8 @@ public class Constants { public static final AccessPermission [] NEWPERMISSIONS = { EXCLUDE, VIEW, CLONE, PUSH, CREATE, DELETE, REWIND }; + public static final AccessPermission [] SSHPERMISSIONS = { VIEW, CLONE, PUSH }; + public static AccessPermission LEGACY = REWIND; public final String code; diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index 77f818c3..ae4588ae 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import com.gitblit.Constants.AccessPermission; import com.gitblit.Keys; import com.gitblit.manager.IRuntimeManager; import com.google.common.base.Charsets; @@ -105,8 +106,18 @@ public class FileKeyManager extends IPublicKeyManager { // skip comments continue; } - SshKey key = new SshKey(entry); - list.add(key); + String [] parts = entry.split(" ", 2); + AccessPermission perm = AccessPermission.fromCode(parts[0]); + if (perm.equals(AccessPermission.NONE)) { + // ssh-rsa DATA COMMENT + SshKey key = new SshKey(entry); + list.add(key); + } else if (perm.exceeds(AccessPermission.NONE)) { + // PERMISSION ssh-rsa DATA COMMENT + SshKey key = new SshKey(parts[1]); + key.setPermission(perm); + list.add(key); + } } if (list.isEmpty()) { @@ -129,7 +140,6 @@ public class FileKeyManager extends IPublicKeyManager { @Override public boolean addKey(String username, SshKey key) { try { - String newKey = stripCommentFromKey(key.getRawData()); boolean replaced = false; List lines = new ArrayList(); File keystore = getKeystore(username); @@ -147,10 +157,10 @@ public class FileKeyManager extends IPublicKeyManager { continue; } - String oldKey = stripCommentFromKey(line); - if (newKey.equals(oldKey)) { + SshKey oldKey = parseKey(line); + if (key.equals(oldKey)) { // replace key - lines.add(key.getRawData()); + lines.add(key.getPermission() + " " + key.getRawData()); replaced = true; } else { // retain key @@ -161,7 +171,7 @@ public class FileKeyManager extends IPublicKeyManager { if (!replaced) { // new key, append - lines.add(key.getRawData()); + lines.add(key.getPermission() + " " + key.getRawData()); } // write keystore @@ -182,8 +192,6 @@ public class FileKeyManager extends IPublicKeyManager { @Override public boolean removeKey(String username, SshKey key) { try { - String rmKey = stripCommentFromKey(key.getRawData()); - File keystore = getKeystore(username); if (keystore.exists()) { List lines = new ArrayList(); @@ -201,8 +209,8 @@ public class FileKeyManager extends IPublicKeyManager { } // only include keys that are NOT rmKey - String oldKey = stripCommentFromKey(line); - if (!rmKey.equals(oldKey)) { + SshKey oldKey = parseKey(line); + if (!key.equals(oldKey)) { lines.add(entry); } } @@ -242,10 +250,18 @@ public class FileKeyManager extends IPublicKeyManager { return keys; } - /* Strips the comment from the key data and eliminates whitespace diffs */ - protected String stripCommentFromKey(String data) { - String [] cols = data.split(" ", 3); - String key = Joiner.on(" ").join(cols[0], cols[1]); - return key; + protected SshKey parseKey(String line) { + String [] parts = line.split(" ", 2); + AccessPermission perm = AccessPermission.fromCode(parts[0]); + if (perm.equals(AccessPermission.NONE)) { + // ssh-rsa DATA COMMENT + SshKey key = new SshKey(line); + return key; + } else { + // PERMISSION ssh-rsa DATA COMMENT + SshKey key = new SshKey(parts[1]); + key.setPermission(perm); + return key; + } } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKey.java b/src/main/java/com/gitblit/transport/ssh/SshKey.java index cb5ee097..6ac0cdcb 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKey.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKey.java @@ -2,12 +2,15 @@ package com.gitblit.transport.ssh; import java.io.Serializable; import java.security.PublicKey; +import java.util.Arrays; +import java.util.List; import org.apache.commons.codec.binary.Base64; import org.apache.sshd.common.SshException; import org.apache.sshd.common.util.Buffer; import org.eclipse.jgit.lib.Constants; +import com.gitblit.Constants.AccessPermission; import com.gitblit.utils.StringUtils; /** @@ -30,13 +33,17 @@ public class SshKey implements Serializable { private String toString; + private AccessPermission permission; + public SshKey(String data) { this.rawData = data; + this.permission = AccessPermission.PUSH; } public SshKey(PublicKey key) { this.publicKey = key; this.comment = ""; + this.permission = AccessPermission.PUSH; } public PublicKey getPublicKey() { @@ -78,6 +85,46 @@ public class SshKey implements Serializable { } } + /** + * Returns true if this key may be used to clone or fetch. + * + * @return true if this key can be used to clone or fetch + */ + public boolean canClone() { + return permission.atLeast(AccessPermission.CLONE); + } + + /** + * Returns true if this key may be used to push changes. + * + * @return true if this key can be used to push changes + */ + public boolean canPush() { + return permission.atLeast(AccessPermission.PUSH); + } + + /** + * Returns the access permission for the key. + * + * @return the access permission for the key + */ + public AccessPermission getPermission() { + return permission; + } + + /** + * Control the access permission assigned to this key. + * + * @param value + */ + public void setPermission(AccessPermission value) throws IllegalArgumentException { + List permitted = Arrays.asList(AccessPermission.SSHPERMISSIONS); + if (!permitted.contains(value)) { + throw new IllegalArgumentException("Illegal SSH public key permission specified: " + value); + } + this.permission = value; + } + public String getRawData() { if (rawData == null && publicKey != null) { // build the raw data manually from the public key diff --git a/src/main/java/com/gitblit/transport/ssh/git/Receive.java b/src/main/java/com/gitblit/transport/ssh/git/Receive.java index 94d09985..f0d86f0d 100644 --- a/src/main/java/com/gitblit/transport/ssh/git/Receive.java +++ b/src/main/java/com/gitblit/transport/ssh/git/Receive.java @@ -17,12 +17,17 @@ package com.gitblit.transport.ssh.git; import org.eclipse.jgit.transport.ReceivePack; +import com.gitblit.transport.ssh.SshKey; import com.gitblit.transport.ssh.commands.CommandMetaData; @CommandMetaData(name = "git-receive-pack", description = "Receives pushes from a client", hidden = true) public class Receive extends BaseGitCommand { @Override protected void runImpl() throws Failure { + SshKey key = getContext().getClient().getKey(); + if (key != null && !key.canPush()) { + throw new Failure(1, "Sorry, your SSH public key is not allowed to push changes!"); + } try { ReceivePack rp = receivePackFactory.create(getContext().getClient(), repo); rp.receive(in, out, null); diff --git a/src/main/java/com/gitblit/transport/ssh/git/Upload.java b/src/main/java/com/gitblit/transport/ssh/git/Upload.java index c4dfa808..11a33cef 100644 --- a/src/main/java/com/gitblit/transport/ssh/git/Upload.java +++ b/src/main/java/com/gitblit/transport/ssh/git/Upload.java @@ -17,6 +17,7 @@ package com.gitblit.transport.ssh.git; import org.eclipse.jgit.transport.UploadPack; +import com.gitblit.transport.ssh.SshKey; import com.gitblit.transport.ssh.commands.CommandMetaData; @CommandMetaData(name = "git-upload-pack", description = "Sends packs to a client for clone and fetch", hidden = true) @@ -24,6 +25,10 @@ public class Upload extends BaseGitCommand { @Override protected void runImpl() throws Failure { try { + SshKey key = getContext().getClient().getKey(); + if (key != null && !key.canClone()) { + throw new Failure(1, "Sorry, your SSH public key is not allowed to clone!"); + } UploadPack up = uploadPackFactory.create(getContext().getClient(), repo); up.upload(in, out, null); } catch (Exception e) { diff --git a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java index ad373060..62daec6a 100644 --- a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java @@ -24,6 +24,7 @@ import org.kohsuke.args4j.Option; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.AccessPermission; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.IPublicKeyManager; import com.gitblit.transport.ssh.SshKey; @@ -33,6 +34,7 @@ import com.gitblit.transport.ssh.commands.SshCommand; import com.gitblit.transport.ssh.commands.UsageExample; import com.gitblit.utils.FlipTable; import com.gitblit.utils.FlipTable.Borders; +import com.gitblit.utils.StringUtils; import com.google.common.base.Joiner; /** @@ -54,7 +56,7 @@ public class KeysDispatcher extends DispatchCommand { } @CommandMetaData(name = "add", description = "Add an SSH public key to your account") - @UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd} -", description = "Upload your SSH public key and add it to your account") + @UsageExample(syntax = "cat ~/.ssh/id_rsa.pub | ${ssh} ${cmd}", description = "Upload your SSH public key and add it to your account") public static class AddKey extends BaseKeyCommand { protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -62,12 +64,33 @@ public class KeysDispatcher extends DispatchCommand { @Argument(metaVar = "", usage = "the key(s) to add") private List addKeys = new ArrayList(); + @Option(name = "--permission", aliases = { "-p" }, metaVar = "PERMISSION", usage = "set the key access permission") + private String permission; + + @Override + protected String getUsageText() { + String permissions = Joiner.on(", ").join(AccessPermission.SSHPERMISSIONS); + StringBuilder sb = new StringBuilder(); + sb.append("Valid SSH public key permissions are:\n ").append(permissions); + return sb.toString(); + } + @Override public void run() throws IOException, UnloggedFailure { String username = getContext().getClient().getUsername(); List keys = readKeys(addKeys); for (String key : keys) { SshKey sshKey = parseKey(key); + if (!StringUtils.isEmpty(permission)) { + AccessPermission ap = AccessPermission.fromCode(permission); + if (ap.exceeds(AccessPermission.NONE)) { + try { + sshKey.setPermission(ap); + } catch (IllegalArgumentException e) { + throw new UnloggedFailure(1, e.getMessage()); + } + } + } getKeyManager().addKey(username, sshKey); log.info("added SSH public key for {}", username); } @@ -167,14 +190,15 @@ public class KeysDispatcher extends DispatchCommand { } protected void asTable(List keys) { - String[] headers = { "#", "Fingerprint", "Comment", "Type" }; + String[] headers = { "#", "Fingerprint", "Comment", "Permission", "Type" }; int len = keys == null ? 0 : keys.size(); Object[][] data = new Object[len][]; for (int i = 0; i < len; i++) { // show 1-based index numbers with the fingerprint // this is useful for comparing with "ssh-add -l" SshKey k = keys.get(i); - data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), k.getAlgorithm() }; + data[i] = new Object[] { (i + 1), k.getFingerprint(), k.getComment(), + k.getPermission(), k.getAlgorithm() }; } stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); @@ -211,9 +235,9 @@ public class KeysDispatcher extends DispatchCommand { } protected void asTable(int index, SshKey key) { - String[] headers = { "#", "Fingerprint", "Comment", "Type" }; + String[] headers = { "#", "Fingerprint", "Comment", "Permission", "Type" }; Object[][] data = new Object[1][]; - data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getAlgorithm() }; + data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getPermission(), key.getAlgorithm() }; stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS)); } -- cgit v1.2.3 From f7e97712b5c4edd72cad1e8e54490de02a766224 Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 2 Apr 2014 09:41:44 -0400 Subject: Fix ${baseFolder}/plugins mkdir bug --- src/main/java/com/gitblit/manager/PluginManager.java | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index 7b03f50d..47154b87 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -244,6 +244,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage */ protected boolean download(String url) throws IOException { File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + pFolder.mkdirs(); File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp"); if (tmpFile.exists()) { tmpFile.delete(); -- cgit v1.2.3 From e9872c8ca4d9af41794a851f2f81ed21c65bb85b Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 2 Apr 2014 11:44:31 -0400 Subject: Allow specifying accepted PUSH transports --- releases.moxie | 3 + src/main/distrib/data/gitblit.properties | 10 +++ src/main/java/com/gitblit/Constants.java | 19 ++++++ src/main/java/com/gitblit/GitBlit.java | 76 ++++++++++++++++++++++ .../com/gitblit/git/GitblitReceivePackFactory.java | 46 +++++++++++++ .../java/com/gitblit/models/RepositoryUrl.java | 3 + 6 files changed, 157 insertions(+) (limited to 'src') diff --git a/releases.moxie b/releases.moxie index 89a7a5f3..32da215c 100644 --- a/releases.moxie +++ b/releases.moxie @@ -23,10 +23,12 @@ r22: { - Redirect to summary page on edit repository (issue-405) - Option to allow LDAP users to directly authenticate without performing LDAP searches (pr-162) - Replace JCommander with args4j to be consistent with other tools (ticket-28) + - Sort repository urls by descending permissions and by transport security within equal permissions additions: - Added an SSH daemon with public key authentication (issue-369, ticket-6) - Added beginnings of a plugin framework for extending Gitblit (issue-381, ticket-23) - Added a French translation (pr-163) + - Added a setting to control what transports may be used for pushes dependencyChanges: - args4j 2.0.26 - JGit 3.3.1 @@ -41,6 +43,7 @@ r22: { settings: - { name: 'realm.ldap.bindpattern', defaultValue: ' ' } - { name: 'tickets.closeOnPushCommitMessageRegex', defaultValue: '(?:fixes|closes)[\\s-]+#?(\\d+)' } + - { name: 'git.acceptedPushTransports', defaultValue: ' ' } - { name: 'git.sshPort', defaultValue: '29418' } - { name: 'git.sshBindInterface', defaultValue: ' ' } - { name: 'git.sshKeysManager', defaultValue: 'com.gitblit.transport.ssh.FileKeyManager' } diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index c52423b9..c7e3a217 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -173,6 +173,16 @@ git.certificateUsernameOIDs = CN # SINCE 0.9.0 git.onlyAccessBareRepositories = false + +# Specify the list of acceptable transports for pushes. +# If this setting is empty, all transports are acceptable. +# +# Valid choices are: GIT HTTP HTTPS SSH +# +# SINCE 1.5.0 +# SPACE-DELIMITED +git.acceptedPushTransports = HTTP HTTPS SSH + # Allow an authenticated user to create a destination repository on a push if # the repository does not already exist. # diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java index 26e0de3c..af533996 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java @@ -540,6 +540,25 @@ public class Constants { } } + public static enum Transport { + // ordered for url advertisements, assuming equal access permissions + SSH, HTTPS, HTTP, GIT; + + public static Transport fromString(String value) { + for (Transport t : values()) { + if (t.name().equalsIgnoreCase(value)) { + return t; + } + } + return null; + } + + public static Transport fromUrl(String url) { + String scheme = url.substring(0, url.indexOf("://")); + return fromString(scheme); + } + } + @Documented @Retention(RetentionPolicy.RUNTIME) public @interface Unused { diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java index 26ab3f3b..08342521 100644 --- a/src/main/java/com/gitblit/GitBlit.java +++ b/src/main/java/com/gitblit/GitBlit.java @@ -17,12 +17,17 @@ package com.gitblit; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import javax.inject.Singleton; import javax.servlet.http.HttpServletRequest; import com.gitblit.Constants.AccessPermission; +import com.gitblit.Constants.Transport; import com.gitblit.manager.GitblitManager; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.manager.IFederationManager; @@ -116,6 +121,32 @@ public class GitBlit extends GitblitManager { return new Object [] { new GitBlitModule()}; } + protected boolean acceptPush(Transport byTransport) { + if (byTransport == null) { + logger.info("Unknown transport, push rejected!"); + return false; + } + + Set transports = new HashSet(); + for (String value : getSettings().getStrings(Keys.git.acceptedPushTransports)) { + Transport transport = Transport.fromString(value); + if (transport == null) { + logger.info(String.format("Ignoring unknown registered transport %s", value)); + continue; + } + + transports.add(transport); + } + + if (transports.isEmpty()) { + // no transports are explicitly specified, all are acceptable + return true; + } + + // verify that the transport is permitted + return transports.contains(byTransport); + } + /** * Returns a list of repository URLs and the user access permission. * @@ -137,6 +168,12 @@ public class GitBlit extends GitblitManager { if (settings.getBoolean(Keys.git.enableGitServlet, true)) { AccessPermission permission = user.getRepositoryPermission(repository).permission; if (permission.exceeds(AccessPermission.NONE)) { + Transport transport = Transport.fromString(request.getScheme()); + if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(transport)) { + // downgrade the repo permission for this transport + // because it is not an acceptable PUSH transport + permission = AccessPermission.CLONE; + } list.add(new RepositoryUrl(getRepositoryUrl(request, username, repository), permission)); } } @@ -146,6 +183,12 @@ public class GitBlit extends GitblitManager { if (!StringUtils.isEmpty(sshDaemonUrl)) { AccessPermission permission = user.getRepositoryPermission(repository).permission; if (permission.exceeds(AccessPermission.NONE)) { + if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(Transport.SSH)) { + // downgrade the repo permission for this transport + // because it is not an acceptable PUSH transport + permission = AccessPermission.CLONE; + } + list.add(new RepositoryUrl(sshDaemonUrl, permission)); } } @@ -155,6 +198,11 @@ public class GitBlit extends GitblitManager { if (!StringUtils.isEmpty(gitDaemonUrl)) { AccessPermission permission = servicesManager.getGitDaemonAccessPermission(user, repository); if (permission.exceeds(AccessPermission.NONE)) { + if (permission.atLeast(AccessPermission.PUSH) && !acceptPush(Transport.GIT)) { + // downgrade the repo permission for this transport + // because it is not an acceptable PUSH transport + permission = AccessPermission.CLONE; + } list.add(new RepositoryUrl(gitDaemonUrl, permission)); } } @@ -173,6 +221,34 @@ public class GitBlit extends GitblitManager { list.add(new RepositoryUrl(MessageFormat.format(url, repository.name), null)); } } + + // sort transports by highest permission and then by transport security + Collections.sort(list, new Comparator() { + + @Override + public int compare(RepositoryUrl o1, RepositoryUrl o2) { + if (!o1.isExternal() && o2.isExternal()) { + // prefer Gitblit over external + return -1; + } else if (o1.isExternal() && !o2.isExternal()) { + // prefer Gitblit over external + return 1; + } else if (o1.isExternal() && o2.isExternal()) { + // sort by Transport ordinal + return o1.transport.compareTo(o2.transport); + } else if (o1.permission.exceeds(o2.permission)) { + // prefer highest permission + return -1; + } else if (o2.permission.exceeds(o1.permission)) { + // prefer highest permission + return 1; + } + + // prefer more secure transports + return o1.transport.compareTo(o2.transport); + } + }); + return list; } diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java index 41e348ba..afda23b0 100644 --- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java +++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java @@ -15,6 +15,9 @@ */ package com.gitblit.git; +import java.util.HashSet; +import java.util.Set; + import javax.servlet.http.HttpServletRequest; import org.eclipse.jgit.lib.PersonIdent; @@ -26,6 +29,7 @@ import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Constants.Transport; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.manager.IGitblit; @@ -66,6 +70,7 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { String origin = ""; String gitblitUrl = ""; int timeout = 0; + Transport transport = null; if (req instanceof HttpServletRequest) { // http/https request may or may not be authenticated @@ -82,6 +87,13 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { user = u; } } + + // determine the transport + if ("http".equals(client.getScheme())) { + transport = Transport.HTTP; + } else if ("https".equals(client.getScheme())) { + transport = Transport.HTTPS; + } } else if (req instanceof GitDaemonClient) { // git daemon request is always anonymous GitDaemonClient client = (GitDaemonClient) req; @@ -90,12 +102,20 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { // set timeout from Git daemon timeout = client.getDaemon().getTimeout(); + + transport = Transport.GIT; } else if (req instanceof SshDaemonClient) { // SSH request is always authenticated SshDaemonClient client = (SshDaemonClient) req; repositoryName = client.getRepositoryName(); origin = client.getRemoteAddress().toString(); user = client.getUser(); + + transport = Transport.SSH; + } + + if (!acceptPush(transport)) { + throw new ServiceNotAuthorizedException(); } boolean allowAnonymousPushes = settings.getBoolean(Keys.git.allowAnonymousPushes, false); @@ -125,4 +145,30 @@ public class GitblitReceivePackFactory implements ReceivePackFactory { return rp; } + + protected boolean acceptPush(Transport byTransport) { + if (byTransport == null) { + logger.info("Unknown transport, push rejected!"); + return false; + } + + Set transports = new HashSet(); + for (String value : gitblit.getSettings().getStrings(Keys.git.acceptedPushTransports)) { + Transport transport = Transport.fromString(value); + if (transport == null) { + logger.info(String.format("Ignoring unknown registered transport %s", value)); + continue; + } + + transports.add(transport); + } + + if (transports.isEmpty()) { + // no transports are explicitly specified, all are acceptable + return true; + } + + // verify that the transport is permitted + return transports.contains(byTransport); + } } \ No newline at end of file diff --git a/src/main/java/com/gitblit/models/RepositoryUrl.java b/src/main/java/com/gitblit/models/RepositoryUrl.java index a24def57..d155dbda 100644 --- a/src/main/java/com/gitblit/models/RepositoryUrl.java +++ b/src/main/java/com/gitblit/models/RepositoryUrl.java @@ -18,6 +18,7 @@ package com.gitblit.models; import java.io.Serializable; import com.gitblit.Constants.AccessPermission; +import com.gitblit.Constants.Transport; /** * Represents a git repository url and it's associated access permission for the @@ -30,10 +31,12 @@ public class RepositoryUrl implements Serializable { private static final long serialVersionUID = 1L; + public final Transport transport; public final String url; public final AccessPermission permission; public RepositoryUrl(String url, AccessPermission permission) { + this.transport = Transport.fromUrl(url); this.url = url; this.permission = permission; } -- cgit v1.2.3 From d85bfb7c9bdfdffbe2c598e9a2f34b6d92cc99d1 Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 2 Apr 2014 11:45:14 -0400 Subject: Improve ticket propose instructions based on primary repository url --- .../com/gitblit/wicket/GitBlitWebApp.properties | 2 ++ .../java/com/gitblit/wicket/pages/TicketPage.java | 27 +++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index 2ab023ff..aeb2d9ef 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -670,3 +670,5 @@ gb.repositoryDoesNotAcceptPatchsets = This repository does not accept patchsets. gb.serverDoesNotAcceptPatchsets = This server does not accept patchsets. gb.ticketIsClosed = This ticket is closed. gb.mergeToDescription = default integration branch for merging ticket patchsets +gb.anonymousCanNotPropose = Anonymous users can not propose patchsets. +gb.youDoNotHaveClonePermission = You are not permitted to clone this repository. \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java index 8571b088..e4bb41fd 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java @@ -54,7 +54,6 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.URIish; import com.gitblit.Constants; import com.gitblit.Constants.AccessPermission; @@ -734,16 +733,17 @@ public class TicketPage extends TicketBasePage { */ if (currentPatchset == null) { // no patchset available - String repoUrl = getRepositoryUrl(user, repository); - if (ticket.isOpen() && app().tickets().isAcceptingNewPatchsets(repository) && !StringUtils.isEmpty(repoUrl)) { + RepositoryUrl repoUrl = getRepositoryUrl(user, repository); + boolean canPropose = repoUrl != null && repoUrl.permission.atLeast(AccessPermission.CLONE) && !UserModel.ANONYMOUS.equals(user); + if (ticket.isOpen() && app().tickets().isAcceptingNewPatchsets(repository) && canPropose) { // ticket & repo will accept a proposal patchset // show the instructions for proposing a patchset Fragment changeIdFrag = new Fragment("patchset", "proposeFragment", this); changeIdFrag.add(new Label("proposeInstructions", MarkdownUtils.transformMarkdown(getString("gb.proposeInstructions"))).setEscapeModelStrings(false)); changeIdFrag.add(new Label("ptWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Barnum"))); - changeIdFrag.add(new Label("ptWorkflowSteps", getProposeWorkflow("propose_pt.md", repoUrl, ticket.number)).setEscapeModelStrings(false)); + changeIdFrag.add(new Label("ptWorkflowSteps", getProposeWorkflow("propose_pt.md", repoUrl.url, ticket.number)).setEscapeModelStrings(false)); changeIdFrag.add(new Label("gitWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Git"))); - changeIdFrag.add(new Label("gitWorkflowSteps", getProposeWorkflow("propose_git.md", repoUrl, ticket.number)).setEscapeModelStrings(false)); + changeIdFrag.add(new Label("gitWorkflowSteps", getProposeWorkflow("propose_git.md", repoUrl.url, ticket.number)).setEscapeModelStrings(false)); add(changeIdFrag); } else { // explain why you can't propose a patchset @@ -757,6 +757,12 @@ public class TicketPage extends TicketBasePage { reason = getString("gb.repositoryIsFrozen"); } else if (!repository.acceptNewPatchsets) { reason = getString("gb.repositoryDoesNotAcceptPatchsets"); + } else if (!canPropose) { + if (UserModel.ANONYMOUS.equals(user)) { + reason = getString("gb.anonymousCanNotPropose"); + } else { + reason = getString("gb.youDoNotHaveClonePermission"); + } } else { reason = getString("gb.serverDoesNotAcceptPatchsets"); } @@ -1476,19 +1482,14 @@ public class TicketPage extends TicketBasePage { * @param repository * @return the primary repository url */ - protected String getRepositoryUrl(UserModel user, RepositoryModel repository) { + protected RepositoryUrl getRepositoryUrl(UserModel user, RepositoryModel repository) { HttpServletRequest req = ((WebRequest) getRequest()).getHttpServletRequest(); List urls = app().gitblit().getRepositoryUrls(req, user, repository); if (ArrayUtils.isEmpty(urls)) { return null; } - String primaryurl = urls.get(0).url; - String url = primaryurl; - try { - url = new URIish(primaryurl).setUser(null).toString(); - } catch (Exception e) { - } - return url; + RepositoryUrl primary = urls.get(0); + return primary; } /** -- cgit v1.2.3 From a9be3d2fb5e69ce3c9ac7b3963853cb338a0ca10 Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 2 Apr 2014 13:54:23 -0400 Subject: Add hostname and ssh repo url methodso to SshCommand --- .../gitblit/transport/ssh/commands/SshCommand.java | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java index 67e2805f..7008b5eb 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java @@ -16,11 +16,19 @@ package com.gitblit.transport.ssh.commands; import java.io.IOException; import java.io.PrintWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.text.MessageFormat; import org.apache.sshd.server.Environment; +import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gitblit.Keys; +import com.gitblit.manager.IGitblit; +import com.gitblit.utils.StringUtils; + public abstract class SshCommand extends BaseCommand { protected Logger log = LoggerFactory.getLogger(getClass()); @@ -45,5 +53,35 @@ public abstract class SshCommand extends BaseCommand { }); } + protected String getHostname() { + IGitblit gitblit = getContext().getGitblit(); + String host = null; + String url = gitblit.getSettings().getString(Keys.web.canonicalUrl, "https://localhost:8443"); + if (url != null) { + try { + host = new URL(url).getHost(); + } catch (MalformedURLException e) { + } + } + if (StringUtils.isEmpty(host)) { + host = SystemReader.getInstance().getHostname(); + } + return host; + } + + protected String getRepositoryUrl(String repository) { + String username = getContext().getClient().getUsername(); + String hostname = getHostname(); + int port = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 0); + if (port == 22) { + // standard port + return MessageFormat.format("{0}@{1}/{2}.git", username, hostname, repository); + } else { + // non-standard port + return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/{3}", + username, hostname, port, repository); + } + } + protected abstract void run() throws UnloggedFailure, Failure, Exception; } -- cgit v1.2.3 From 521cb6022a9ee30bf3115a8dcb991aa5c7e420e3 Mon Sep 17 00:00:00 2001 From: James Moger Date: Mon, 7 Apr 2014 22:57:47 -0400 Subject: Unit tests for ssh daemon and keys dispatcher --- .../ssh/CachingPublicKeyAuthenticator.java | 27 ++-- .../com/gitblit/transport/ssh/FileKeyManager.java | 4 +- .../gitblit/transport/ssh/IPublicKeyManager.java | 7 +- .../gitblit/transport/ssh/MemoryKeyManager.java | 20 ++- .../java/com/gitblit/transport/ssh/SshKey.java | 4 +- .../transport/ssh/SshServerSessionFactory.java | 4 +- .../gitblit/transport/ssh/keys/KeysDispatcher.java | 52 ++++---- src/main/java/log4j.properties | 4 +- src/test/java/com/gitblit/tests/GitBlitSuite.java | 18 ++- src/test/java/com/gitblit/tests/SshDaemonTest.java | 134 ++++++++------------ .../com/gitblit/tests/SshKeysDispatcherTest.java | 115 +++++++++++++++++ src/test/java/com/gitblit/tests/SshUnitTest.java | 141 +++++++++++++++++++++ src/test/java/com/gitblit/tests/SshUtils.java | 74 ----------- 13 files changed, 388 insertions(+), 216 deletions(-) create mode 100644 src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java create mode 100644 src/test/java/com/gitblit/tests/SshUnitTest.java delete mode 100644 src/test/java/com/gitblit/tests/SshUtils.java (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java index 48e5aa28..4ce26d0f 100644 --- a/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java +++ b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java @@ -38,8 +38,7 @@ import com.google.common.base.Preconditions; * @author Eric Myrhe * */ -public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, - SessionListener { +public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, SessionListener { protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -47,18 +46,15 @@ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, protected final IAuthenticationManager authManager; - private final Map> cache = - new ConcurrentHashMap>(); + private final Map> cache = new ConcurrentHashMap>(); - public CachingPublicKeyAuthenticator(IPublicKeyManager keyManager, - IAuthenticationManager authManager) { + public CachingPublicKeyAuthenticator(IPublicKeyManager keyManager, IAuthenticationManager authManager) { this.keyManager = keyManager; this.authManager = authManager; } @Override - public boolean authenticate(String username, PublicKey key, - ServerSession session) { + public boolean authenticate(String username, PublicKey key, ServerSession session) { Map map = cache.get(session); if (map == null) { map = new HashMap(); @@ -73,19 +69,21 @@ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, return result; } - private boolean doAuthenticate(String username, PublicKey suppliedKey, - ServerSession session) { + private boolean doAuthenticate(String username, PublicKey suppliedKey, ServerSession session) { SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY); Preconditions.checkState(client.getUser() == null); username = username.toLowerCase(Locale.US); List keys = keyManager.getKeys(username); - if (keys == null || keys.isEmpty()) { - log.info("{} has not added any public keys for ssh authentication", - username); + if (keys.isEmpty()) { + log.info("{} has not added any public keys for ssh authentication", username); return false; } + SshKey pk = new SshKey(suppliedKey); + log.debug("auth supplied {}", pk.getFingerprint()); + for (SshKey key : keys) { + log.debug("auth compare to {}", key.getFingerprint()); if (key.equals(suppliedKey)) { UserModel user = authManager.authenticate(username, key); if (user != null) { @@ -96,8 +94,7 @@ public class CachingPublicKeyAuthenticator implements PublickeyAuthenticator, } } - log.warn("could not authenticate {} for SSH using the supplied public key", - username); + log.warn("could not authenticate {} for SSH using the supplied public key", username); return false; } diff --git a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java index ae4588ae..a063dc7d 100644 --- a/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java @@ -90,7 +90,7 @@ public class FileKeyManager extends IPublicKeyManager { @Override protected List getKeysImpl(String username) { try { - log.info("loading keystore for {}", username); + log.info("loading ssh keystore for {}", username); File keystore = getKeystore(username); if (!keystore.exists()) { return null; @@ -128,7 +128,7 @@ public class FileKeyManager extends IPublicKeyManager { return list; } } catch (IOException e) { - throw new RuntimeException("Canot read ssh keys", e); + throw new RuntimeException("Cannot read ssh keys", e); } return null; } diff --git a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java index 956a76ef..0dbee637 100644 --- a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java @@ -16,6 +16,7 @@ package com.gitblit.transport.ssh; import java.text.MessageFormat; +import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -46,7 +47,11 @@ public abstract class IPublicKeyManager implements IManager { .build(new CacheLoader>() { @Override public List load(String username) { - return getKeysImpl(username); + List keys = getKeysImpl(username); + if (keys == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(keys); } }); diff --git a/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java index 18f9a4e1..357b34a2 100644 --- a/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java @@ -28,7 +28,7 @@ import java.util.Map; */ public class MemoryKeyManager extends IPublicKeyManager { - Map> keys; + final Map> keys; public MemoryKeyManager() { keys = new HashMap>(); @@ -57,7 +57,8 @@ public class MemoryKeyManager extends IPublicKeyManager { @Override protected boolean isStale(String username) { - return false; + // always return true so we gets keys from our hashmap + return true; } @Override @@ -75,6 +76,7 @@ public class MemoryKeyManager extends IPublicKeyManager { if (!keys.containsKey(id)) { keys.put(id, new ArrayList()); } + log.info("added {} key {}", username, key.getFingerprint()); return keys.get(id).add(key); } @@ -82,15 +84,27 @@ public class MemoryKeyManager extends IPublicKeyManager { public boolean removeKey(String username, SshKey key) { String id = username.toLowerCase(); if (!keys.containsKey(id)) { + log.info("can't remove keys for {}", username); return false; } - return keys.get(id).remove(key); + List list = keys.get(id); + boolean success = list.remove(key); + if (success) { + log.info("removed {} key {}", username, key.getFingerprint()); + } + + if (list.isEmpty()) { + keys.remove(id); + log.info("no {} keys left, removed {}", username, username); + } + return success; } @Override public boolean removeAllKeys(String username) { String id = username.toLowerCase(); keys.remove(id.toLowerCase()); + log.info("removed all keys for {}", username); return true; } } diff --git a/src/main/java/com/gitblit/transport/ssh/SshKey.java b/src/main/java/com/gitblit/transport/ssh/SshKey.java index 6ac0cdcb..6a20d7dd 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshKey.java +++ b/src/main/java/com/gitblit/transport/ssh/SshKey.java @@ -155,8 +155,8 @@ public class SshKey implements Serializable { final byte [] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1])); hash = StringUtils.getMD5(bin); } else { - // TODO get hash from publickey - hash = "todo"; + // TODO calculate the correct hash from a PublicKey instance + hash = StringUtils.getMD5(getPublicKey().getEncoded()); } for (int i = 0; i < hash.length(); i += 2) { sb.append(hash.charAt(i)).append(hash.charAt(i + 1)).append(':'); diff --git a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java index dd3c139d..0c018f02 100644 --- a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java +++ b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java @@ -41,7 +41,7 @@ public class SshServerSessionFactory extends SessionFactory { @Override protected AbstractSession createSession(final IoSession io) throws Exception { - log.info("connection accepted on " + io); + log.info("creating ssh session from {}", io.getRemoteAddress()); if (io instanceof MinaSession) { if (((MinaSession) io).getSession().getConfig() instanceof SocketSessionConfig) { @@ -59,7 +59,7 @@ public class SshServerSessionFactory extends SessionFactory { session.addCloseSessionListener(new SshFutureListener() { @Override public void operationComplete(CloseFuture future) { - log.info("connection closed on " + io); + log.info("closed ssh session from {}", io.getRemoteAddress()); } }); return session; diff --git a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java index 62daec6a..3f581462 100644 --- a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java @@ -61,7 +61,7 @@ public class KeysDispatcher extends DispatchCommand { protected final Logger log = LoggerFactory.getLogger(getClass()); - @Argument(metaVar = "", usage = "the key(s) to add") + @Argument(metaVar = "", usage = "the key to add") private List addKeys = new ArrayList(); @Option(name = "--permission", aliases = { "-p" }, metaVar = "PERMISSION", usage = "set the key access permission") @@ -76,7 +76,7 @@ public class KeysDispatcher extends DispatchCommand { } @Override - public void run() throws IOException, UnloggedFailure { + public void run() throws IOException, Failure { String username = getContext().getClient().getUsername(); List keys = readKeys(addKeys); for (String key : keys) { @@ -87,7 +87,7 @@ public class KeysDispatcher extends DispatchCommand { try { sshKey.setPermission(ap); } catch (IllegalArgumentException e) { - throw new UnloggedFailure(1, e.getMessage()); + throw new Failure(1, e.getMessage()); } } } @@ -105,22 +105,21 @@ public class KeysDispatcher extends DispatchCommand { private final String ALL = "ALL"; - @Argument(metaVar = "||ALL", usage = "the key to remove", required = true) - private List removeKeys = new ArrayList(); + @Argument(metaVar = "|ALL", usage = "the key to remove", required = true) + private List keyParameters = new ArrayList(); @Override - public void run() throws IOException, UnloggedFailure { + public void run() throws IOException, Failure { String username = getContext().getClient().getUsername(); // remove a key that has been piped to the command // or remove all keys - List currentKeys = getKeyManager().getKeys(username); - if (currentKeys == null || currentKeys.isEmpty()) { + List registeredKeys = new ArrayList(getKeyManager().getKeys(username)); + if (registeredKeys.isEmpty()) { throw new UnloggedFailure(1, "There are no registered keys!"); } - List keys = readKeys(removeKeys); - if (keys.contains(ALL)) { + if (keyParameters.contains(ALL)) { if (getKeyManager().removeAllKeys(username)) { stdout.println("Removed all keys."); log.info("removed all SSH public keys from {}", username); @@ -128,32 +127,25 @@ public class KeysDispatcher extends DispatchCommand { log.warn("failed to remove all SSH public keys from {}", username); } } else { - for (String key : keys) { + for (String keyParameter : keyParameters) { try { // remove a key by it's index (1-based indexing) - int index = Integer.parseInt(key); - if (index > keys.size()) { - if (keys.size() == 1) { - throw new UnloggedFailure(1, "Invalid index specified. There is only 1 registered key."); + int index = Integer.parseInt(keyParameter); + if (index > registeredKeys.size()) { + if (keyParameters.size() == 1) { + throw new Failure(1, "Invalid index specified. There is only 1 registered key."); } - throw new UnloggedFailure(1, String.format("Invalid index specified. There are %d registered keys.", keys.size())); + throw new Failure(1, String.format("Invalid index specified. There are %d registered keys.", registeredKeys.size())); } - SshKey sshKey = currentKeys.get(index - 1); + SshKey sshKey = registeredKeys.get(index - 1); if (getKeyManager().removeKey(username, sshKey)) { stdout.println(String.format("Removed %s", sshKey.getFingerprint())); } else { - throw new UnloggedFailure(1, String.format("failed to remove #%s: %s", key, sshKey.getFingerprint())); - } - } catch (Exception e) { - // remove key by raw key data - SshKey sshKey = parseKey(key); - if (getKeyManager().removeKey(username, sshKey)) { - stdout.println(String.format("Removed %s", sshKey.getFingerprint())); - log.info("removed SSH public key {} from {}", sshKey.getFingerprint(), username); - } else { - log.warn("failed to remove SSH public key {} from {}", sshKey.getFingerprint(), username); - throw new UnloggedFailure(1, String.format("failed to remove %s", sshKey.getFingerprint())); + throw new Failure(1, String.format("failed to remove #%s: %s", keyParameter, sshKey.getFingerprint())); } + } catch (NumberFormatException e) { + log.warn("failed to remove SSH public key {} from {}", keyParameter, username); + throw new Failure(1, String.format("failed to remove key %s", keyParameter)); } } } @@ -254,7 +246,7 @@ public class KeysDispatcher extends DispatchCommand { private List values = new ArrayList(); @Override - public void run() throws UnloggedFailure { + public void run() throws Failure { final String username = getContext().getClient().getUsername(); IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager(); List keys = keyManager.getKeys(username); @@ -268,7 +260,7 @@ public class KeysDispatcher extends DispatchCommand { if (keyManager.addKey(username, key)) { stdout.println(String.format("Updated the comment for key #%d.", index)); } else { - throw new UnloggedFailure(1, String.format("Failed to update the comment for key #%d!", index)); + throw new Failure(1, String.format("Failed to update the comment for key #%d!", index)); } } diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties index 115dcd01..43d31d80 100644 --- a/src/main/java/log4j.properties +++ b/src/main/java/log4j.properties @@ -25,7 +25,9 @@ log4j.rootCategory=INFO, S #log4j.logger.net=INFO #log4j.logger.com.gitblit=DEBUG -log4j.logger.org.apache.sshd=ERROR +log4j.logger.com.gitblit.transport.ssh.SshServerSession=WARN +log4j.logger.org.apache.sshd=WARN +log4j.logger.org.apache.mina=WARN log4j.logger.org.apache.wicket=INFO log4j.logger.org.apache.wicket.RequestListenerInterface=WARN diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java index b8d3b181..5a7dcea1 100644 --- a/src/test/java/com/gitblit/tests/GitBlitSuite.java +++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java @@ -64,7 +64,8 @@ import com.gitblit.utils.JGitUtils; SshDaemonTest.class, GroovyScriptTest.class, LuceneExecutorTest.class, RepositoryModelTest.class, FanoutServiceTest.class, Issue0259Test.class, Issue0271Test.class, HtpasswdAuthenticationTest.class, ModelUtilsTest.class, JnaUtilsTest.class, LdapSyncServiceTest.class, FileTicketServiceTest.class, - BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class }) + BranchTicketServiceTest.class, RedisTicketServiceTest.class, AuthenticationManagerTest.class, + SshKeysDispatcherTest.class }) public class GitBlitSuite { public static final File BASEFOLDER = new File("data"); @@ -137,11 +138,16 @@ public class GitBlitSuite { Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { - GitBlitServer.main("--httpPort", "" + port, "--httpsPort", "0", "--shutdownPort", - "" + shutdownPort, "--gitPort", "" + gitPort, "--repositoriesFolder", - "\"" + GitBlitSuite.REPOSITORIES.getAbsolutePath() + "\"", "--userService", - GitBlitSuite.USERSCONF.getAbsolutePath(), "--settings", GitBlitSuite.SETTINGS.getAbsolutePath(), - "--baseFolder", "data", "--sshPort", "" + sshPort); + GitBlitServer.main( + "--httpPort", "" + port, + "--httpsPort", "0", + "--shutdownPort", "" + shutdownPort, + "--gitPort", "" + gitPort, + "--sshPort", "" + sshPort, + "--repositoriesFolder", GitBlitSuite.REPOSITORIES.getAbsolutePath(), + "--userService", GitBlitSuite.USERSCONF.getAbsolutePath(), + "--settings", GitBlitSuite.SETTINGS.getAbsolutePath(), + "--baseFolder", "data"); } }); diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java index 620190ef..dcaeaff8 100644 --- a/src/test/java/com/gitblit/tests/SshDaemonTest.java +++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java @@ -15,102 +15,76 @@ */ package com.gitblit.tests; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.security.KeyPair; -import java.util.concurrent.atomic.AtomicBoolean; +import java.io.File; +import java.text.MessageFormat; +import java.util.List; -import org.apache.sshd.ClientChannel; import org.apache.sshd.ClientSession; import org.apache.sshd.SshClient; -import org.apache.sshd.common.KeyPairProvider; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; +import org.eclipse.jgit.api.CloneCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.util.FileUtils; import org.junit.Test; import com.gitblit.Constants; -import com.gitblit.transport.ssh.IPublicKeyManager; -import com.gitblit.transport.ssh.MemoryKeyManager; -import com.gitblit.transport.ssh.SshKey; +import com.gitblit.Constants.AccessRestrictionType; +import com.gitblit.Constants.AuthorizationControl; +import com.gitblit.models.RepositoryModel; +import com.gitblit.utils.JGitUtils; -public class SshDaemonTest extends GitblitUnitTest { +public class SshDaemonTest extends SshUnitTest { - private static final AtomicBoolean started = new AtomicBoolean(false); - private static KeyPair pair; + static File ticgitFolder = new File(GitBlitSuite.REPOSITORIES, "working/ticgit"); - @BeforeClass - public static void startGitblit() throws Exception { - started.set(GitBlitSuite.startGitblit()); - pair = SshUtils.createTestHostKeyProvider().loadKey(KeyPairProvider.SSH_RSA); - } - - @AfterClass - public static void stopGitblit() throws Exception { - if (started.get()) { - GitBlitSuite.stopGitblit(); - } - } - - protected MemoryKeyManager getKeyManager() { - IPublicKeyManager mgr = gitblit().getPublicKeyManager(); - if (mgr instanceof MemoryKeyManager) { - return (MemoryKeyManager) gitblit().getPublicKeyManager(); - } else { - throw new RuntimeException("unexpected key manager type " + mgr.getClass().getName()); - } - } - - @Before - public void prepare() { - MemoryKeyManager keyMgr = getKeyManager(); - keyMgr.addKey("admin", new SshKey(pair.getPublic())); - } - - @After - public void tearDown() { - MemoryKeyManager keyMgr = getKeyManager(); - keyMgr.removeAllKeys("admin"); - } + String url = GitBlitSuite.sshDaemonUrl; @Test public void testPublicKeyAuthentication() throws Exception { - SshClient client = SshClient.setUpDefaultClient(); - client.start(); - ClientSession session = client.connect("localhost", GitBlitSuite.sshPort).await().getSession(); - pair.getPublic().getEncoded(); - assertTrue(session.authPublicKey("admin", pair).await().isSuccess()); + SshClient client = getClient(); + ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).await().getSession(); + session.addPublicKeyIdentity(rwKeyPair); + assertTrue(session.auth().await().isSuccess()); } @Test public void testVersionCommand() throws Exception { - SshClient client = SshClient.setUpDefaultClient(); - client.start(); - ClientSession session = client.connect("localhost", GitBlitSuite.sshPort).await().getSession(); - pair.getPublic().getEncoded(); - assertTrue(session.authPublicKey("admin", pair).await().isSuccess()); - - ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, "version"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - Writer w = new OutputStreamWriter(baos); - w.close(); - channel.setIn(new ByteArrayInputStream(baos.toByteArray())); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ByteArrayOutputStream err = new ByteArrayOutputStream(); - channel.setOut(out); - channel.setErr(err); - channel.open(); - - channel.waitFor(ClientChannel.CLOSED, 0); + String result = testSshCommand("version"); + assertEquals(Constants.getGitBlitVersion(), result); + } - String result = out.toString().trim(); - channel.close(false); - client.stop(); + @Test + public void testCloneCommand() throws Exception { + if (ticgitFolder.exists()) { + GitBlitSuite.close(ticgitFolder); + FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE); + } - assertEquals(Constants.getGitBlitVersion(), result); - } + // set clone restriction + RepositoryModel model = repositories().getRepositoryModel("ticgit.git"); + model.accessRestriction = AccessRestrictionType.CLONE; + model.authorizationControl = AuthorizationControl.NAMED; + repositories().updateRepositoryModel(model.name, model, false); + + JschConfigTestSessionFactory sessionFactory = new JschConfigTestSessionFactory(roKeyPair); + SshSessionFactory.setInstance(sessionFactory); + + CloneCommand clone = Git.cloneRepository(); + clone.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password)); + clone.setURI(MessageFormat.format("{0}/ticgit.git", url)); + clone.setDirectory(ticgitFolder); + clone.setBare(false); + clone.setCloneAllBranches(true); + Git git = clone.call(); + List commits = JGitUtils.getRevLog(git.getRepository(), 10); + GitBlitSuite.close(git); + assertEquals(10, commits.size()); + + // restore anonymous repository access + model.accessRestriction = AccessRestrictionType.NONE; + model.authorizationControl = AuthorizationControl.NAMED; + repositories().updateRepositoryModel(model.name, model, false); + } } diff --git a/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java b/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java new file mode 100644 index 00000000..8ccdc5bf --- /dev/null +++ b/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.tests; + +import java.security.KeyPair; +import java.util.List; + +import org.junit.Test; +import org.parboiled.common.StringUtils; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.transport.ssh.SshKey; + +/** + * Tests the Keys Dispatcher and it's commands. + * + * @author James Moger + * + */ +public class SshKeysDispatcherTest extends SshUnitTest { + + @Test + public void testKeysListCommand() throws Exception { + String result = testSshCommand("keys ls -L"); + List keys = getKeyManager().getKeys(username); + assertEquals(String.format("There are %d keys!", keys.size()), 2, keys.size()); + assertEquals(keys.get(0).getRawData() + "\n" + keys.get(1).getRawData(), result); + } + + @Test + public void testKeysWhichCommand() throws Exception { + String result = testSshCommand("keys which -L"); + List keys = getKeyManager().getKeys(username); + assertEquals(String.format("There are %d keys!", keys.size()), 2, keys.size()); + assertEquals(keys.get(0).getRawData(), result); + } + + @Test + public void testKeysRmCommand() throws Exception { + testSshCommand("keys rm 2"); + String result = testSshCommand("keys ls -L"); + List keys = getKeyManager().getKeys(username); + assertEquals(String.format("There are %d keys!", keys.size()), 1, keys.size()); + assertEquals(keys.get(0).getRawData(), result); + } + + @Test + public void testKeysRmAllByIndexCommand() throws Exception { + testSshCommand("keys rm 1 2"); + List keys = getKeyManager().getKeys(username); + assertEquals(String.format("There are %d keys!", keys.size()), 0, keys.size()); + try { + testSshCommand("keys ls -L"); + assertTrue("Authentication worked without a public key?!", false); + } catch (AssertionError e) { + assertTrue(true); + } + } + + @Test + public void testKeysRmAllCommand() throws Exception { + testSshCommand("keys rm ALL"); + List keys = getKeyManager().getKeys(username); + assertEquals(String.format("There are %d keys!", keys.size()), 0, keys.size()); + try { + testSshCommand("keys ls -L"); + assertTrue("Authentication worked without a public key?!", false); + } catch (AssertionError e) { + assertTrue(true); + } + } + + @Test + public void testKeysAddCommand() throws Exception { + KeyPair kp = generator.generateKeyPair(); + SshKey key = new SshKey(kp.getPublic()); + testSshCommand("keys add --permission R", key.getRawData()); + List keys = getKeyManager().getKeys(username); + assertEquals(String.format("There are %d keys!", keys.size()), 3, keys.size()); + assertEquals(AccessPermission.CLONE, keys.get(2).getPermission()); + + String result = testSshCommand("keys ls -L"); + StringBuilder sb = new StringBuilder(); + for (SshKey sk : keys) { + sb.append(sk.getRawData()); + sb.append('\n'); + } + sb.setLength(sb.length() - 1); + assertEquals(sb.toString(), result); + } + + @Test + public void testKeysCommentCommand() throws Exception { + List keys = getKeyManager().getKeys(username); + assertTrue(StringUtils.isEmpty(keys.get(0).getComment())); + String comment = "this is my comment"; + testSshCommand(String.format("keys comment 1 %s", comment)); + + keys = getKeyManager().getKeys(username); + assertEquals(comment, keys.get(0).getComment()); + } +} diff --git a/src/test/java/com/gitblit/tests/SshUnitTest.java b/src/test/java/com/gitblit/tests/SshUnitTest.java new file mode 100644 index 00000000..43b51b74 --- /dev/null +++ b/src/test/java/com/gitblit/tests/SshUnitTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2014 gitblit.com. + * + * Licensed 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 com.gitblit.tests; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.SocketAddress; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PublicKey; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.ClientChannel; +import org.apache.sshd.ClientSession; +import org.apache.sshd.SshClient; +import org.apache.sshd.client.ServerKeyVerifier; +import org.apache.sshd.common.util.SecurityUtils; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +import com.gitblit.Constants.AccessPermission; +import com.gitblit.transport.ssh.IPublicKeyManager; +import com.gitblit.transport.ssh.MemoryKeyManager; +import com.gitblit.transport.ssh.SshKey; + +/** + * Base class for SSH unit tests. + */ +public abstract class SshUnitTest extends GitblitUnitTest { + + protected static final AtomicBoolean started = new AtomicBoolean(false); + protected static KeyPairGenerator generator; + protected KeyPair rwKeyPair; + protected KeyPair roKeyPair; + protected String username = "admin"; + protected String password = "admin"; + + @BeforeClass + public static void startGitblit() throws Exception { + generator = SecurityUtils.getKeyPairGenerator("RSA"); + started.set(GitBlitSuite.startGitblit()); + } + + @AfterClass + public static void stopGitblit() throws Exception { + if (started.get()) { + GitBlitSuite.stopGitblit(); + } + } + + protected MemoryKeyManager getKeyManager() { + IPublicKeyManager mgr = gitblit().getPublicKeyManager(); + if (mgr instanceof MemoryKeyManager) { + return (MemoryKeyManager) gitblit().getPublicKeyManager(); + } else { + throw new RuntimeException("unexpected key manager type " + mgr.getClass().getName()); + } + } + + @Before + public void prepare() { + rwKeyPair = generator.generateKeyPair(); + + MemoryKeyManager keyMgr = getKeyManager(); + keyMgr.addKey(username, new SshKey(rwKeyPair.getPublic())); + + roKeyPair = generator.generateKeyPair(); + SshKey sshKey = new SshKey(roKeyPair.getPublic()); + sshKey.setPermission(AccessPermission.CLONE); + keyMgr.addKey(username, sshKey); + } + + @After + public void tearDown() { + MemoryKeyManager keyMgr = getKeyManager(); + keyMgr.removeAllKeys(username); + } + + protected SshClient getClient() { + SshClient client = SshClient.setUpDefaultClient(); + client.setServerKeyVerifier(new ServerKeyVerifier() { + @Override + public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) { + return true; + } + }); + client.start(); + return client; + } + + protected String testSshCommand(String cmd) throws IOException, InterruptedException { + return testSshCommand(cmd, null); + } + + protected String testSshCommand(String cmd, String stdin) throws IOException, InterruptedException { + SshClient client = getClient(); + ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).await().getSession(); + session.addPublicKeyIdentity(rwKeyPair); + assertTrue(session.auth().await().isSuccess()); + + ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, cmd); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + if (stdin != null) { + Writer w = new OutputStreamWriter(baos); + w.write(stdin); + w.close(); + } + channel.setIn(new ByteArrayInputStream(baos.toByteArray())); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + channel.setOut(out); + channel.setErr(err); + channel.open(); + + channel.waitFor(ClientChannel.CLOSED, 0); + + String result = out.toString().trim(); + channel.close(false); + client.stop(); + return result; + } +} diff --git a/src/test/java/com/gitblit/tests/SshUtils.java b/src/test/java/com/gitblit/tests/SshUtils.java deleted file mode 100644 index 9760f755..00000000 --- a/src/test/java/com/gitblit/tests/SshUtils.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 com.gitblit.tests; - -import java.io.File; -import java.net.ServerSocket; -import java.net.URISyntaxException; -import java.net.URL; - -import org.apache.sshd.common.KeyPairProvider; -import org.apache.sshd.common.keyprovider.FileKeyPairProvider; -import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; - -public class SshUtils { - - public static KeyPairProvider createTestHostKeyProvider() { - return new SimpleGeneratorHostKeyProvider("target/hostkey.rsa", "RSA"); - } - - public static FileKeyPairProvider createTestKeyPairProvider(String resource) { - return new FileKeyPairProvider(new String[] { getFile(resource) }); - } - - public static int getFreePort() throws Exception { - ServerSocket s = new ServerSocket(0); - try { - return s.getLocalPort(); - } finally { - s.close(); - } - } - - private static String getFile(String resource) { - URL url = SshUtils.class.getClassLoader().getResource(resource); - File f; - try { - f = new File(url.toURI()); - } catch(URISyntaxException e) { - f = new File(url.getPath()); - } - return f.toString(); - } - - public static void deleteRecursive(File file) { - if (file != null) { - if (file.isDirectory()) { - File[] children = file.listFiles(); - if (children != null) { - for (File child : children) { - deleteRecursive(child); - } - } - } - file.delete(); - } - } - -} -- cgit v1.2.3 From f5ca1655278d8b34a402120864a5a5c628b1c31d Mon Sep 17 00:00:00 2001 From: James Moger Date: Thu, 10 Apr 2014 16:57:32 -0400 Subject: Rename SSH key store on user rename --- src/main/java/com/gitblit/manager/GitblitManager.java | 3 +++ .../java/com/gitblit/transport/ssh/IPublicKeyManager.java | 11 +++++++++++ 2 files changed, 14 insertions(+) (limited to 'src') diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index 5a7d15ae..e3b6cf7d 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -338,6 +338,9 @@ public class GitblitManager implements IGitblit { repositoryManager.updateRepositoryModel(model.name, model, false); } } + + // rename the user's ssh public keystore + getPublicKeyManager().renameUser(username, user.username); } if (!userManager.updateUserModel(username, user)) { throw new GitBlitException("Failed to update user!"); diff --git a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java index 0dbee637..1e74b2f0 100644 --- a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java +++ b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java @@ -79,6 +79,17 @@ public abstract class IPublicKeyManager implements IManager { return null; } + public final void renameUser(String oldName, String newName) { + List keys = getKeys(oldName); + if (keys == null || keys.isEmpty()) { + return; + } + removeAllKeys(oldName); + for (SshKey key : keys) { + addKey(newName, key); + } + } + protected abstract boolean isStale(String username); protected abstract List getKeysImpl(String username); -- cgit v1.2.3 From 556f69edca2ef8409fa9ea3b336681130ff7a609 Mon Sep 17 00:00:00 2001 From: James Moger Date: Thu, 10 Apr 2014 16:58:06 -0400 Subject: Clarify interactive shells message --- src/main/java/com/gitblit/transport/ssh/WelcomeShell.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java index 4341a3ea..acd3c15f 100644 --- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java +++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java @@ -134,7 +134,7 @@ public class WelcomeShell implements Factory { msg.append(user.getDisplayName()); msg.append(", you have successfully connected over SSH."); msg.append(nl); - msg.append(" Interactive shells are disabled."); + msg.append(" Interactive shells are not available."); msg.append(nl); msg.append(nl); msg.append(" client: "); -- cgit v1.2.3 From d433fd3ab6185c2fb5eab998d1fcab4dd0940e5f Mon Sep 17 00:00:00 2001 From: James Moger Date: Thu, 10 Apr 2014 16:58:26 -0400 Subject: Documentation --- src/main/distrib/data/gitblit.properties | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index c7e3a217..3a531232 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -129,7 +129,9 @@ git.sshKeysFolder= ${baseFolder}/ssh # SINCE 1.5.0 git.sshBackend = NIO2 -# Number of thread for starting SSH commands. +# Number of threads used to parse a command line submitted by a client over SSH +# for execution, create the internal data structures used by that command, +# and schedule it for execution on another thread. # # SINCE 1.5.0 git.sshCommandStartThreads = 2 -- cgit v1.2.3 From b2fec20f1f1081607b54b3e7dd20b12d03cef113 Mon Sep 17 00:00:00 2001 From: James Moger Date: Thu, 10 Apr 2014 17:33:19 -0400 Subject: Improve plugin manager based on upstreamed contributions to pf4j --- .classpath | 2 +- build.moxie | 2 +- gitblit.iml | 6 +- .../java/com/gitblit/manager/GitblitManager.java | 76 ++-- .../java/com/gitblit/manager/IPluginManager.java | 103 ++++- .../java/com/gitblit/manager/PluginManager.java | 329 +++++++++++--- .../java/com/gitblit/models/PluginRegistry.java | 21 +- .../transport/ssh/commands/PluginDispatcher.java | 478 ++++++++++++++------- .../transport/ssh/commands/RootDispatcher.java | 7 +- src/main/java/com/gitblit/utils/StringUtils.java | 2 +- 10 files changed, 740 insertions(+), 286 deletions(-) (limited to 'src') diff --git a/.classpath b/.classpath index 252a7c96..bcf2fbd8 100644 --- a/.classpath +++ b/.classpath @@ -76,7 +76,7 @@ - + diff --git a/build.moxie b/build.moxie index eb2878a8..bc4d7d5e 100644 --- a/build.moxie +++ b/build.moxie @@ -174,7 +174,7 @@ dependencies: - compile 'args4j:args4j:2.0.26' :war :fedclient :authority - compile 'commons-codec:commons-codec:1.7' :war - compile 'redis.clients:jedis:2.3.1' :war -- compile 'ro.fortsoft.pf4j:pf4j:0.6' :war +- compile 'ro.fortsoft.pf4j:pf4j:0.7.0' :war - test 'junit' # Dependencies for Selenium web page testing - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar diff --git a/gitblit.iml b/gitblit.iml index ed067f28..0d5fb690 100644 --- a/gitblit.iml +++ b/gitblit.iml @@ -791,13 +791,13 @@ - + - + - + diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index e3b6cf7d..191d7cf1 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -42,9 +42,8 @@ import org.eclipse.jgit.transport.RefSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ro.fortsoft.pf4j.PluginClassLoader; +import ro.fortsoft.pf4j.PluginState; import ro.fortsoft.pf4j.PluginWrapper; -import ro.fortsoft.pf4j.RuntimeMode; import com.gitblit.Constants; import com.gitblit.Constants.AccessPermission; @@ -61,6 +60,7 @@ import com.gitblit.models.ForkModel; import com.gitblit.models.GitClientApplication; import com.gitblit.models.Mailing; import com.gitblit.models.Metric; +import com.gitblit.models.PluginRegistry.InstallState; import com.gitblit.models.PluginRegistry.PluginRegistration; import com.gitblit.models.PluginRegistry.PluginRelease; import com.gitblit.models.ProjectModel; @@ -1190,92 +1190,92 @@ public class GitblitManager implements IGitblit { */ @Override - public List getExtensions(Class clazz) { - return pluginManager.getExtensions(clazz); + public void startPlugins() { + pluginManager.startPlugins(); } @Override - public PluginWrapper whichPlugin(Class clazz) { - return pluginManager.whichPlugin(clazz); + public void stopPlugins() { + pluginManager.stopPlugins(); } @Override - public boolean deletePlugin(PluginWrapper wrapper) { - return pluginManager.deletePlugin(wrapper); + public List getPlugins() { + return pluginManager.getPlugins(); } @Override - public boolean refreshRegistry() { - return pluginManager.refreshRegistry(); + public PluginWrapper getPlugin(String pluginId) { + return pluginManager.getPlugin(pluginId); } @Override - public boolean installPlugin(String url) { - return pluginManager.installPlugin(url); + public List> getExtensionClasses(String pluginId) { + return pluginManager.getExtensionClasses(pluginId); } @Override - public boolean installPlugin(PluginRelease pv) { - return pluginManager.installPlugin(pv); + public List getExtensions(Class clazz) { + return pluginManager.getExtensions(clazz); } @Override - public List getRegisteredPlugins() { - return pluginManager.getRegisteredPlugins(); + public PluginWrapper whichPlugin(Class clazz) { + return pluginManager.whichPlugin(clazz); } @Override - public PluginRegistration lookupPlugin(String idOrName) { - return pluginManager.lookupPlugin(idOrName); + public PluginState startPlugin(String pluginId) { + return pluginManager.startPlugin(pluginId); } @Override - public PluginRelease lookupRelease(String idOrName, String version) { - return pluginManager.lookupRelease(idOrName, version); + public PluginState stopPlugin(String pluginId) { + return pluginManager.stopPlugin(pluginId); } @Override - public List getPlugins() { - return pluginManager.getPlugins(); + public boolean disablePlugin(String pluginId) { + return pluginManager.disablePlugin(pluginId); } @Override - public List getResolvedPlugins() { - return pluginManager.getResolvedPlugins(); + public boolean enablePlugin(String pluginId) { + return pluginManager.enablePlugin(pluginId); } @Override - public List getUnresolvedPlugins() { - return pluginManager.getUnresolvedPlugins(); + public boolean deletePlugin(String pluginId) { + return pluginManager.deletePlugin(pluginId); } @Override - public List getStartedPlugins() { - return pluginManager.getStartedPlugins(); + public boolean refreshRegistry() { + return pluginManager.refreshRegistry(); } @Override - public void loadPlugins() { - pluginManager.loadPlugins(); + public boolean installPlugin(String url, boolean verifyChecksum) throws IOException { + return pluginManager.installPlugin(url, verifyChecksum); } @Override - public void startPlugins() { - pluginManager.startPlugins(); + public List getRegisteredPlugins() { + return pluginManager.getRegisteredPlugins(); } @Override - public void stopPlugins() { - pluginManager.stopPlugins(); + public List getRegisteredPlugins(InstallState state) { + return pluginManager.getRegisteredPlugins(state); } @Override - public PluginClassLoader getPluginClassLoader(String pluginId) { - return pluginManager.getPluginClassLoader(pluginId); + public PluginRegistration lookupPlugin(String idOrName) { + return pluginManager.lookupPlugin(idOrName); } @Override - public RuntimeMode getRuntimeMode() { - return pluginManager.getRuntimeMode(); + public PluginRelease lookupRelease(String idOrName, String version) { + return pluginManager.lookupRelease(idOrName, version); } } diff --git a/src/main/java/com/gitblit/manager/IPluginManager.java b/src/main/java/com/gitblit/manager/IPluginManager.java index 1f7f85ee..33763aa8 100644 --- a/src/main/java/com/gitblit/manager/IPluginManager.java +++ b/src/main/java/com/gitblit/manager/IPluginManager.java @@ -15,15 +15,74 @@ */ package com.gitblit.manager; +import java.io.IOException; import java.util.List; -import ro.fortsoft.pf4j.PluginManager; +import ro.fortsoft.pf4j.PluginState; import ro.fortsoft.pf4j.PluginWrapper; +import com.gitblit.models.PluginRegistry.InstallState; import com.gitblit.models.PluginRegistry.PluginRegistration; import com.gitblit.models.PluginRegistry.PluginRelease; -public interface IPluginManager extends IManager, PluginManager { +public interface IPluginManager extends IManager { + + /** + * Starts all plugins. + */ + void startPlugins(); + + /** + * Stops all plugins. + */ + void stopPlugins(); + + /** + * Starts the specified plugin. + * + * @param pluginId + * @return the state of the plugin + */ + PluginState startPlugin(String pluginId); + + /** + * Stops the specified plugin. + * + * @param pluginId + * @return the state of the plugin + */ + PluginState stopPlugin(String pluginId); + + /** + * Returns the list of extensions the plugin provides. + * + * @param type + * @return a list of extensions the plugin provides + */ + List> getExtensionClasses(String pluginId); + + /** + * Returns the list of extension instances for a given extension point. + * + * @param type + * @return a list of extension instances + */ + List getExtensions(Class type); + + /** + * Returns the list of all resolved plugins. + * + * @return a list of resolved plugins + */ + List getPlugins(); + + /** + * Retrieves the {@link PluginWrapper} for the specified plugin id. + * + * @param pluginId + * @return the plugin wrapper + */ + PluginWrapper getPlugin(String pluginId); /** * Retrieves the {@link PluginWrapper} that loaded the given class 'clazz'. @@ -34,27 +93,41 @@ public interface IPluginManager extends IManager, PluginManager { PluginWrapper whichPlugin(Class clazz); /** - * Delete the plugin represented by {@link PluginWrapper}. + * Disable the plugin represented by pluginId. * - * @param wrapper + * @param pluginId * @return true if successful */ - boolean deletePlugin(PluginWrapper wrapper); + boolean disablePlugin(String pluginId); /** - * Refresh the plugin registry. + * Enable the plugin represented by pluginId. + * + * @param pluginId + * @return true if successful */ - boolean refreshRegistry(); + boolean enablePlugin(String pluginId); /** - * Install the plugin from the specified url. + * Delete the plugin represented by pluginId. + * + * @param pluginId + * @return true if successful */ - boolean installPlugin(String url); + boolean deletePlugin(String pluginId); /** - * Install the plugin. + * Refresh the plugin registry. */ - boolean installPlugin(PluginRelease pr); + boolean refreshRegistry(); + + /** + * Install the plugin from the specified url. + * + * @param url + * @param verifyChecksum + */ + boolean installPlugin(String url, boolean verifyChecksum) throws IOException; /** * The list of all registered plugins. @@ -63,6 +136,14 @@ public interface IPluginManager extends IManager, PluginManager { */ List getRegisteredPlugins(); + /** + * Return a list of registered plugins that match the install state. + * + * @param state + * @return the list of plugins that match the install state + */ + List getRegisteredPlugins(InstallState state); + /** * Lookup a plugin registration from the plugin registries. * diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index 47154b87..9cefc88d 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -18,13 +18,18 @@ package com.gitblit.manager; import java.io.BufferedInputStream; import java.io.File; import java.io.FileFilter; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -33,11 +38,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.fortsoft.pf4j.DefaultPluginManager; +import ro.fortsoft.pf4j.PluginClassLoader; +import ro.fortsoft.pf4j.PluginState; +import ro.fortsoft.pf4j.PluginStateEvent; +import ro.fortsoft.pf4j.PluginStateListener; import ro.fortsoft.pf4j.PluginVersion; import ro.fortsoft.pf4j.PluginWrapper; import com.gitblit.Keys; import com.gitblit.models.PluginRegistry; +import com.gitblit.models.PluginRegistry.InstallState; import com.gitblit.models.PluginRegistry.PluginRegistration; import com.gitblit.models.PluginRegistry.PluginRelease; import com.gitblit.utils.Base64; @@ -49,16 +59,18 @@ import com.google.common.io.InputSupplier; /** * The plugin manager maintains the lifecycle of plugins. It is exposed as - * Dagger bean. The extension consumers supposed to retrieve plugin manager - * from the Dagger DI and retrieve extensions provided by active plugins. + * Dagger bean. The extension consumers supposed to retrieve plugin manager from + * the Dagger DI and retrieve extensions provided by active plugins. * * @author David Ostrovsky * */ -public class PluginManager extends DefaultPluginManager implements IPluginManager { +public class PluginManager implements IPluginManager, PluginStateListener { private final Logger logger = LoggerFactory.getLogger(getClass()); + private final DefaultPluginManager pf4j; + private final IRuntimeManager runtimeManager; // timeout defaults of Maven 3.0.4 in seconds @@ -67,47 +79,168 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage private int readTimeout = 12800; public PluginManager(IRuntimeManager runtimeManager) { - super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins")); + File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); this.runtimeManager = runtimeManager; + this.pf4j = new DefaultPluginManager(dir); + } + + @Override + public void pluginStateChanged(PluginStateEvent event) { + logger.debug(event.toString()); } @Override public PluginManager start() { - logger.info("Loading plugins..."); - loadPlugins(); - logger.info("Starting loaded plugins..."); - startPlugins(); + pf4j.loadPlugins(); + logger.debug("Starting plugins"); + pf4j.startPlugins(); return this; } @Override public PluginManager stop() { - logger.info("Stopping loaded plugins..."); - stopPlugins(); + logger.debug("Stopping plugins"); + pf4j.stopPlugins(); return null; } + /** + * Installs the plugin from the url. + * + * @param url + * @param verifyChecksum + * @return true if successful + */ @Override - public boolean deletePlugin(PluginWrapper pw) { - File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); - File pluginFolder = new File(folder, pw.getPluginPath()); - File pluginZip = new File(folder, pw.getPluginPath() + ".zip"); + public synchronized boolean installPlugin(String url, boolean verifyChecksum) throws IOException { + File file = download(url, verifyChecksum); + if (file == null || !file.exists()) { + logger.error("Failed to download plugin {}", url); + return false; + } - if (pluginFolder.exists()) { - FileUtils.delete(pluginFolder); + String pluginId = pf4j.loadPlugin(file); + if (StringUtils.isEmpty(pluginId)) { + logger.error("Failed to load plugin {}", file); + return false; } - if (pluginZip.exists()) { - FileUtils.delete(pluginZip); + + PluginState state = pf4j.startPlugin(pluginId); + return PluginState.STARTED.equals(state); + } + + @Override + public synchronized boolean disablePlugin(String pluginId) { + return pf4j.disablePlugin(pluginId); + } + + @Override + public synchronized boolean enablePlugin(String pluginId) { + if (pf4j.enablePlugin(pluginId)) { + return PluginState.STARTED == pf4j.startPlugin(pluginId); } - return true; + return false; + } + + @Override + public synchronized boolean deletePlugin(String pluginId) { + PluginWrapper pluginWrapper = getPlugin(pluginId); + final String name = pluginWrapper.getPluginPath().substring(1); + if (pf4j.deletePlugin(pluginId)) { + + // delete the checksums + File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + File [] checksums = pFolder.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + if (!file.isFile()) { + return false; + } + + return file.getName().startsWith(name) && + (file.getName().toLowerCase().endsWith(".sha1") + || file.getName().toLowerCase().endsWith(".md5")); + } + + }); + + if (checksums != null) { + for (File checksum : checksums) { + checksum.delete(); + } + } + return true; + } + return false; + } + + @Override + public synchronized PluginState startPlugin(String pluginId) { + return pf4j.startPlugin(pluginId); + } + + @Override + public synchronized PluginState stopPlugin(String pluginId) { + return pf4j.stopPlugin(pluginId); + } + + @Override + public synchronized void startPlugins() { + pf4j.startPlugins(); + } + + @Override + public synchronized void stopPlugins() { + pf4j.stopPlugins(); + } + + @Override + public synchronized List getPlugins() { + return pf4j.getPlugins(); + } + + @Override + public synchronized PluginWrapper getPlugin(String pluginId) { + return pf4j.getPlugin(pluginId); + } + + @Override + public synchronized List> getExtensionClasses(String pluginId) { + List> list = new ArrayList>(); + PluginClassLoader loader = pf4j.getPluginClassLoader(pluginId); + for (String className : pf4j.getExtensionClassNames(pluginId)) { + try { + list.add(loader.loadClass(className)); + } catch (ClassNotFoundException e) { + logger.error(String.format("Failed to find %s in %s", className, pluginId), e); + } + } + return list; } @Override - public boolean refreshRegistry() { + public synchronized List getExtensions(Class type) { + return pf4j.getExtensions(type); + } + + @Override + public synchronized PluginWrapper whichPlugin(Class clazz) { + return pf4j.whichPlugin(clazz); + } + + @Override + public synchronized boolean refreshRegistry() { String dr = "http://gitblit.github.io/gitblit-registry/plugins.json"; String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr); try { - return download(url); + File file = download(url, true); + if (file != null && file.exists()) { + URL selfUrl = new URL(url.substring(0, url.lastIndexOf('/'))); + // replace ${self} with the registry url + String content = FileUtils.readContent(file, "\n"); + content = content.replace("${self}", selfUrl.toString()); + FileUtils.writeContent(file, content); + } } catch (Exception e) { logger.error(String.format("Failed to retrieve plugins.json from %s", url), e); } @@ -124,7 +257,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } }; - File [] files = folder.listFiles(jsonFilter); + File[] files = folder.listFiles(jsonFilter); if (files == null || files.length == 0) { // automatically retrieve the registry if we don't have a local copy refreshRegistry(); @@ -140,6 +273,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage try { String json = FileUtils.readContent(file, "\n"); registry = JsonUtils.fromJsonString(json, PluginRegistry.class); + registry.setup(); } catch (Exception e) { logger.error("Failed to deserialize " + file, e); } @@ -151,18 +285,17 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } @Override - public List getRegisteredPlugins() { + public synchronized List getRegisteredPlugins() { List list = new ArrayList(); Map map = new TreeMap(); for (PluginRegistry registry : getRegistries()) { - List registrations = registry.registrations; - list.addAll(registrations); - for (PluginRegistration reg : registrations) { + list.addAll(registry.registrations); + for (PluginRegistration reg : list) { reg.installedRelease = null; map.put(reg.id, reg); } } - for (PluginWrapper pw : getPlugins()) { + for (PluginWrapper pw : pf4j.getPlugins()) { String id = pw.getDescriptor().getPluginId(); PluginVersion pv = pw.getDescriptor().getVersion(); PluginRegistration reg = map.get(id); @@ -174,75 +307,129 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage } @Override - public PluginRegistration lookupPlugin(String idOrName) { - for (PluginRegistry registry : getRegistries()) { - PluginRegistration reg = registry.lookup(idOrName); - if (reg != null) { - return reg; + public synchronized List getRegisteredPlugins(InstallState state) { + List list = getRegisteredPlugins(); + Iterator itr = list.iterator(); + while (itr.hasNext()) { + if (state != itr.next().getInstallState()) { + itr.remove(); } } - return null; + return list; } @Override - public PluginRelease lookupRelease(String idOrName, String version) { - for (PluginRegistry registry : getRegistries()) { - PluginRegistration reg = registry.lookup(idOrName); - if (reg != null) { - PluginRelease pv; - if (StringUtils.isEmpty(version)) { - pv = reg.getCurrentRelease(); - } else { - pv = reg.getRelease(version); - } - if (pv != null) { - return pv; - } + public synchronized PluginRegistration lookupPlugin(String idOrName) { + for (PluginRegistration reg : getRegisteredPlugins()) { + if (reg.id.equalsIgnoreCase(idOrName) || reg.name.equalsIgnoreCase(idOrName)) { + return reg; } } return null; } - - /** - * Installs the plugin from the plugin version. - * - * @param pv - * @throws IOException - * @return true if successful - */ @Override - public boolean installPlugin(PluginRelease pv) { - return installPlugin(pv.url); + public synchronized PluginRelease lookupRelease(String idOrName, String version) { + PluginRegistration reg = lookupPlugin(idOrName); + if (reg == null) { + return null; + } + + PluginRelease pv; + if (StringUtils.isEmpty(version)) { + pv = reg.getCurrentRelease(); + } else { + pv = reg.getRelease(version); + } + return pv; } /** - * Installs the plugin from the url. + * Downloads a file with optional checksum verification. * * @param url - * @return true if successful + * @param verifyChecksum + * @return + * @throws IOException */ - @Override - public boolean installPlugin(String url) { + protected File download(String url, boolean verifyChecksum) throws IOException { + File file = downloadFile(url); + + File sha1File = null; try { - if (!download(url)) { - return false; - } - // TODO stop, unload, load + sha1File = downloadFile(url + ".sha1"); + } catch (IOException e) { + } + + File md5File = null; + try { + md5File = downloadFile(url + ".md5"); } catch (IOException e) { - logger.error("Failed to install plugin from " + url, e); + } - return true; + + if (sha1File == null && md5File == null && verifyChecksum) { + throw new IOException("Missing SHA1 and MD5 checksums for " + url); + } + + String expected; + MessageDigest md = null; + if (sha1File != null && sha1File.exists()) { + // prefer SHA1 to MD5 + expected = FileUtils.readContent(sha1File, "\n").split(" ")[0].trim(); + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + logger.error(null, e); + } + } else { + expected = FileUtils.readContent(md5File, "\n").split(" ")[0].trim(); + try { + md = MessageDigest.getInstance("MD5"); + } catch (Exception e) { + logger.error(null, e); + } + } + + // calculate the checksum + FileInputStream is = null; + try { + is = new FileInputStream(file); + DigestInputStream dis = new DigestInputStream(is, md); + byte [] buffer = new byte[1024]; + while ((dis.read(buffer)) > -1) { + // read + } + dis.close(); + + byte [] digest = md.digest(); + String calculated = StringUtils.toHex(digest).trim(); + + if (!expected.equals(calculated)) { + String msg = String.format("Invalid checksum for %s\nAlgorithm: %s\nExpected: %s\nCalculated: %s", + file.getAbsolutePath(), + md.getAlgorithm(), + expected, + calculated); + file.delete(); + throw new IOException(msg); + } + } finally { + if (is != null) { + is.close(); + } + } + return file; } /** * Download a file to the plugins folder. * * @param url - * @return + * @return the downloaded file * @throws IOException */ - protected boolean download(String url) throws IOException { + protected File downloadFile(String url) throws IOException { File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); pFolder.mkdirs(); File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp"); @@ -257,9 +444,9 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis()); Files.copy(new InputSupplier() { - @Override + @Override public InputStream getInput() throws IOException { - return new BufferedInputStream(conn.getInputStream()); + return new BufferedInputStream(conn.getInputStream()); } }, tmpFile); @@ -270,7 +457,7 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage tmpFile.renameTo(destFile); destFile.setLastModified(lastModified); - return true; + return destFile; } protected URLConnection getConnection(URL url) throws IOException { diff --git a/src/main/java/com/gitblit/models/PluginRegistry.java b/src/main/java/com/gitblit/models/PluginRegistry.java index c81a0f23..b5cf0ee1 100644 --- a/src/main/java/com/gitblit/models/PluginRegistry.java +++ b/src/main/java/com/gitblit/models/PluginRegistry.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import org.parboiled.common.StringUtils; @@ -37,7 +38,13 @@ public class PluginRegistry implements Serializable { public PluginRegistry(String name) { this.name = name; - registrations = new ArrayList(); + registrations = new CopyOnWriteArrayList(); + } + + public void setup() { + for (PluginRegistration reg : registrations) { + reg.registry = name; + } } public PluginRegistration lookup(String idOrName) { @@ -80,6 +87,8 @@ public class PluginRegistry implements Serializable { public transient String installedRelease; + public transient String registry; + public List releases; public PluginRegistration(String id) { @@ -90,10 +99,12 @@ public class PluginRegistry implements Serializable { public PluginRelease getCurrentRelease() { PluginRelease current = null; if (!StringUtils.isEmpty(currentRelease)) { + // find specified current = getRelease(currentRelease); } if (current == null) { + // find by date Date date = new Date(0); for (PluginRelease pv : releases) { if (pv.date.after(date)) { @@ -135,9 +146,15 @@ public class PluginRegistry implements Serializable { } } - public static class PluginRelease { + public static class PluginRelease implements Comparable { public String version; public Date date; + public String requires; public String url; + + @Override + public int compareTo(PluginRelease o) { + return PluginVersion.createVersion(version).compareTo(PluginVersion.createVersion(o.version)); + } } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java index ba6f30d6..99dd6d13 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java @@ -15,23 +15,26 @@ */ package com.gitblit.transport.ssh.commands; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; +import ro.fortsoft.pf4j.ExtensionPoint; import ro.fortsoft.pf4j.PluginDependency; import ro.fortsoft.pf4j.PluginDescriptor; import ro.fortsoft.pf4j.PluginState; import ro.fortsoft.pf4j.PluginWrapper; import com.gitblit.manager.IGitblit; +import com.gitblit.models.PluginRegistry.InstallState; import com.gitblit.models.PluginRegistry.PluginRegistration; import com.gitblit.models.PluginRegistry.PluginRelease; import com.gitblit.models.UserModel; import com.gitblit.utils.FlipTable; import com.gitblit.utils.FlipTable.Borders; +import com.google.common.base.Joiner; /** * The plugin dispatcher and commands for runtime plugin management. @@ -47,13 +50,16 @@ public class PluginDispatcher extends DispatchCommand { register(user, ListPlugins.class); register(user, StartPlugin.class); register(user, StopPlugin.class); + register(user, EnablePlugin.class); + register(user, DisablePlugin.class); register(user, ShowPlugin.class); - register(user, RemovePlugin.class); - register(user, InstallPlugin.class); + register(user, RefreshPlugins.class); register(user, AvailablePlugins.class); + register(user, InstallPlugin.class); + register(user, UninstallPlugin.class); } - @CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins") + @CommandMetaData(name = "list", aliases = { "ls" }, description = "List plugins") public static class ListPlugins extends ListCommand { @Override @@ -67,7 +73,7 @@ public class PluginDispatcher extends DispatchCommand { protected void asTable(List list) { String[] headers; if (verbose) { - String [] h = { "#", "Id", "Version", "State", "Mode", "Path", "Provider"}; + String [] h = { "#", "Id", "Version", "State", "Path", "Provider"}; headers = h; } else { String [] h = { "#", "Id", "Version", "State", "Path"}; @@ -78,7 +84,7 @@ public class PluginDispatcher extends DispatchCommand { PluginWrapper p = list.get(i); PluginDescriptor d = p.getDescriptor(); if (verbose) { - data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getRuntimeMode(), p.getPluginPath(), d.getProvider() }; + data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getPluginPath(), d.getProvider() }; } else { data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getPluginPath() }; } @@ -92,7 +98,7 @@ public class PluginDispatcher extends DispatchCommand { for (PluginWrapper pw : list) { PluginDescriptor d = pw.getDescriptor(); if (verbose) { - outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getRuntimeMode(), pw.getPluginPath(), d.getProvider()); + outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath(), d.getProvider()); } else { outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath()); } @@ -100,146 +106,265 @@ public class PluginDispatcher extends DispatchCommand { } } + static abstract class PluginCommand extends SshCommand { + + protected PluginWrapper getPlugin(String id) throws Failure { + IGitblit gitblit = getContext().getGitblit(); + PluginWrapper pluginWrapper = null; + try { + int index = Integer.parseInt(id); + List plugins = gitblit.getPlugins(); + if (index > plugins.size()) { + throw new UnloggedFailure(1, "Invalid plugin index specified!"); + } + pluginWrapper = plugins.get(index - 1); + } catch (NumberFormatException e) { + pluginWrapper = gitblit.getPlugin(id); + if (pluginWrapper == null) { + PluginRegistration reg = gitblit.lookupPlugin(id); + if (reg == null) { + throw new UnloggedFailure("Invalid plugin specified!"); + } + pluginWrapper = gitblit.getPlugin(reg.id); + } + } + + return pluginWrapper; + } + } + @CommandMetaData(name = "start", description = "Start a plugin") - public static class StartPlugin extends SshCommand { + public static class StartPlugin extends PluginCommand { @Argument(index = 0, required = true, metaVar = "ALL|", usage = "the plugin to start") - protected String plugin; + protected String id; @Override - public void run() throws UnloggedFailure { + public void run() throws Failure { IGitblit gitblit = getContext().getGitblit(); - if (plugin.equalsIgnoreCase("ALL")) { + if (id.equalsIgnoreCase("ALL")) { gitblit.startPlugins(); stdout.println("All plugins started"); } else { - try { - int index = Integer.parseInt(plugin); - List plugins = gitblit.getPlugins(); - if (index > plugins.size()) { - throw new UnloggedFailure(1, "Invalid plugin index specified!"); - } - PluginWrapper pw = plugins.get(index - 1); - start(pw); - } catch (NumberFormatException n) { - for (PluginWrapper pw : gitblit.getPlugins()) { - PluginDescriptor pd = pw.getDescriptor(); - if (pd.getPluginId().equalsIgnoreCase(plugin)) { - start(pw); - break; - } - } + PluginWrapper pluginWrapper = getPlugin(id); + if (pluginWrapper == null) { + throw new UnloggedFailure(String.format("Plugin %s is not installed!", id)); } - } - } - protected void start(PluginWrapper pw) throws UnloggedFailure { - String id = pw.getDescriptor().getPluginId(); - if (pw.getPluginState() == PluginState.STARTED) { - throw new UnloggedFailure(1, String.format("%s is already started.", id)); - } - try { - pw.getPlugin().start(); -// pw.setPluginState(PluginState.STARTED); - stdout.println(String.format("%s started", id)); - } catch (Exception pe) { - throw new UnloggedFailure(1, String.format("Failed to start %s", id), pe); + PluginState state = gitblit.startPlugin(pluginWrapper.getPluginId()); + if (PluginState.STARTED.equals(state)) { + stdout.println(String.format("Started %s", pluginWrapper.getPluginId())); + } else { + throw new Failure(1, String.format("Failed to start %s", pluginWrapper.getPluginId())); + } } } } - @CommandMetaData(name = "stop", description = "Stop a plugin") - public static class StopPlugin extends SshCommand { + public static class StopPlugin extends PluginCommand { @Argument(index = 0, required = true, metaVar = "ALL|", usage = "the plugin to stop") - protected String plugin; + protected String id; @Override - public void run() throws UnloggedFailure { + public void run() throws Failure { IGitblit gitblit = getContext().getGitblit(); - if (plugin.equalsIgnoreCase("ALL")) { + if (id.equalsIgnoreCase("ALL")) { gitblit.stopPlugins(); stdout.println("All plugins stopped"); } else { - try { - int index = Integer.parseInt(plugin); - List plugins = gitblit.getPlugins(); - if (index > plugins.size()) { - throw new UnloggedFailure(1, "Invalid plugin index specified!"); + PluginWrapper pluginWrapper = getPlugin(id); + if (pluginWrapper == null) { + throw new UnloggedFailure(String.format("Plugin %s is not installed!", id)); } - PluginWrapper pw = plugins.get(index - 1); - stop(pw); - } catch (NumberFormatException n) { - for (PluginWrapper pw : gitblit.getPlugins()) { - PluginDescriptor pd = pw.getDescriptor(); - if (pd.getPluginId().equalsIgnoreCase(plugin)) { - stop(pw); - break; - } + + PluginState state = gitblit.stopPlugin(pluginWrapper.getPluginId()); + if (PluginState.STOPPED.equals(state)) { + stdout.println(String.format("Stopped %s", pluginWrapper.getPluginId())); + } else { + throw new Failure(1, String.format("Failed to stop %s", pluginWrapper.getPluginId())); } } + } + } + + @CommandMetaData(name = "enable", description = "Enable a plugin") + public static class EnablePlugin extends PluginCommand { + + @Argument(index = 0, required = true, metaVar = "", usage = "the plugin id to enable") + protected String id; + + @Override + public void run() throws Failure { + IGitblit gitblit = getContext().getGitblit(); + PluginWrapper pluginWrapper = getPlugin(id); + if (pluginWrapper == null) { + throw new UnloggedFailure("Invalid plugin specified!"); + } + + if (gitblit.enablePlugin(pluginWrapper.getPluginId())) { + stdout.println(String.format("Enabled %s", pluginWrapper.getPluginId())); + } else { + throw new Failure(1, String.format("Failed to enable %s", pluginWrapper.getPluginId())); } } + } + + @CommandMetaData(name = "disable", description = "Disable a plugin") + public static class DisablePlugin extends PluginCommand { - protected void stop(PluginWrapper pw) throws UnloggedFailure { - String id = pw.getDescriptor().getPluginId(); - if (pw.getPluginState() == PluginState.STOPPED) { - throw new UnloggedFailure(1, String.format("%s is already stopped.", id)); + @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to disable") + protected String id; + + @Override + public void run() throws Failure { + IGitblit gitblit = getContext().getGitblit(); + PluginWrapper pluginWrapper = getPlugin(id); + if (pluginWrapper == null) { + throw new UnloggedFailure("Invalid plugin specified!"); } - try { - pw.getPlugin().stop(); -// pw.setPluginState(PluginState.STOPPED); - stdout.println(String.format("%s stopped", id)); - } catch (Exception pe) { - throw new UnloggedFailure(1, String.format("Failed to stop %s", id), pe); + + if (gitblit.disablePlugin(pluginWrapper.getPluginId())) { + stdout.println(String.format("Disabled %s", pluginWrapper.getPluginId())); + } else { + throw new Failure(1, String.format("Failed to disable %s", pluginWrapper.getPluginId())); } } } @CommandMetaData(name = "show", description = "Show the details of a plugin") - public static class ShowPlugin extends SshCommand { + public static class ShowPlugin extends PluginCommand { - @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to stop") - protected int index; + @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to show") + protected String id; @Override - public void run() throws UnloggedFailure { + public void run() throws Failure { IGitblit gitblit = getContext().getGitblit(); - List plugins = gitblit.getPlugins(); - if (index > plugins.size()) { - throw new UnloggedFailure(1, "Invalid plugin index specified!"); + PluginWrapper pw = getPlugin(id); + if (pw == null) { + PluginRegistration registration = gitblit.lookupPlugin(id); + if (registration == null) { + throw new Failure(1, String.format("Unknown plugin %s", id)); + } + show(registration); + } else { + show(pw); + } + } + + protected String buildFieldTable(PluginWrapper pw, PluginRegistration reg) { + final String id = pw == null ? reg.id : pw.getPluginId(); + final String name = reg == null ? "" : reg.name; + final String version = pw == null ? "" : pw.getDescriptor().getVersion().toString(); + final String provider = pw == null ? reg.provider : pw.getDescriptor().getProvider(); + final String registry = reg == null ? "" : reg.registry; + final String path = pw == null ? "" : pw.getPluginPath(); + final String projectUrl = reg == null ? "" : reg.projectUrl; + final String state; + if (pw == null) { + // plugin could be installed + state = InstallState.NOT_INSTALLED.toString(); + } else if (reg == null) { + // unregistered, installed plugin + state = Joiner.on(", ").join(InstallState.INSTALLED, pw.getPluginState()); + } else { + // registered, installed plugin + state = Joiner.on(", ").join(reg.getInstallState(), pw.getPluginState()); } - PluginWrapper pw = plugins.get(index - 1); - PluginDescriptor d = pw.getDescriptor(); - // FIELDS StringBuilder sb = new StringBuilder(); - sb.append("Version : ").append(d.getVersion()).append('\n'); - sb.append("Provider : ").append(d.getProvider()).append('\n'); - sb.append("Path : ").append(pw.getPluginPath()).append('\n'); - sb.append("State : ").append(pw.getPluginState()).append('\n'); - final String fields = sb.toString(); + sb.append("ID : ").append(id).append('\n'); + sb.append("Version : ").append(version).append('\n'); + sb.append("State : ").append(state).append('\n'); + sb.append("Path : ").append(path).append('\n'); + sb.append('\n'); + sb.append("Name : ").append(name).append('\n'); + sb.append("Provider : ").append(provider).append('\n'); + sb.append("Project URL : ").append(projectUrl).append('\n'); + sb.append("Registry : ").append(registry).append('\n'); + + return sb.toString(); + } - // TODO EXTENSIONS - sb.setLength(0); - List exts = new ArrayList(); + protected String buildReleaseTable(PluginRegistration reg) { + List releases = reg.releases; + Collections.sort(releases); + String releaseTable; + if (releases.isEmpty()) { + releaseTable = FlipTable.EMPTY; + } else { + String[] headers = { "Version", "Date", "Requires" }; + Object[][] data = new Object[releases.size()][]; + for (int i = 0; i < releases.size(); i++) { + PluginRelease release = releases.get(i); + data[i] = new Object[] { (release.version.equals(reg.installedRelease) ? ">" : " ") + release.version, + release.date, release.requires }; + } + releaseTable = FlipTable.of(headers, data, Borders.COLS); + } + return releaseTable; + } + + /** + * Show an uninstalled plugin. + * + * @param reg + */ + protected void show(PluginRegistration reg) { + // REGISTRATION + final String fields = buildFieldTable(null, reg); + final String releases = buildReleaseTable(reg); + + String[] headers = { reg.id }; + Object[][] data = new Object[3][]; + data[0] = new Object[] { fields }; + data[1] = new Object[] { "RELEASES" }; + data[2] = new Object[] { releases }; + stdout.println(FlipTable.of(headers, data)); + } + + /** + * Show an installed plugin. + * + * @param pw + */ + protected void show(PluginWrapper pw) { + IGitblit gitblit = getContext().getGitblit(); + PluginRegistration reg = gitblit.lookupPlugin(pw.getPluginId()); + + // FIELDS + final String fields = buildFieldTable(pw, reg); + + // EXTENSIONS + StringBuilder sb = new StringBuilder(); + List> exts = gitblit.getExtensionClasses(pw.getPluginId()); String extensions; if (exts.isEmpty()) { extensions = FlipTable.EMPTY; } else { - String[] headers = { "Id", "Version" }; - Object[][] data = new Object[exts.size()][]; + StringBuilder description = new StringBuilder(); for (int i = 0; i < exts.size(); i++) { - String ext = exts.get(i); - data[0] = new Object[] { ext.toString(), ext.toString() }; + Class ext = exts.get(i); + if (ext.isAnnotationPresent(CommandMetaData.class)) { + CommandMetaData meta = ext.getAnnotation(CommandMetaData.class); + description.append(meta.name()); + if (meta.description().length() > 0) { + description.append(": ").append(meta.description()); + } + description.append('\n'); + } + description.append(ext.getName()).append("\n └ "); + description.append(getExtensionPoint(ext).getName()); + description.append("\n\n"); } - extensions = FlipTable.of(headers, data, Borders.COLS); + extensions = description.toString(); } // DEPENDENCIES sb.setLength(0); - List deps = d.getDependencies(); + List deps = pw.getDescriptor().getDependencies(); String dependencies; if (deps.isEmpty()) { dependencies = FlipTable.EMPTY; @@ -248,80 +373,47 @@ public class PluginDispatcher extends DispatchCommand { Object[][] data = new Object[deps.size()][]; for (int i = 0; i < deps.size(); i++) { PluginDependency dep = deps.get(i); - data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() }; + data[i] = new Object[] { dep.getPluginId(), dep.getPluginVersion() }; } dependencies = FlipTable.of(headers, data, Borders.COLS); } - String[] headers = { d.getPluginId() }; - Object[][] data = new Object[5][]; + // RELEASES + String releases; + if (reg == null) { + releases = FlipTable.EMPTY; + } else { + releases = buildReleaseTable(reg); + } + + String[] headers = { pw.getPluginId() }; + Object[][] data = new Object[7][]; data[0] = new Object[] { fields }; data[1] = new Object[] { "EXTENSIONS" }; data[2] = new Object[] { extensions }; data[3] = new Object[] { "DEPENDENCIES" }; data[4] = new Object[] { dependencies }; + data[5] = new Object[] { "RELEASES" }; + data[6] = new Object[] { releases }; stdout.println(FlipTable.of(headers, data)); } - } - @CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true) - public static class RemovePlugin extends SshCommand { - - @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to stop") - protected int index; - - @Override - public void run() throws UnloggedFailure { - IGitblit gitblit = getContext().getGitblit(); - List plugins = gitblit.getPlugins(); - if (index > plugins.size()) { - throw new UnloggedFailure(1, "Invalid plugin index specified!"); - } - PluginWrapper pw = plugins.get(index - 1); - PluginDescriptor d = pw.getDescriptor(); - if (gitblit.deletePlugin(pw)) { - stdout.println(String.format("Deleted %s %s", d.getPluginId(), d.getVersion())); - } else { - throw new UnloggedFailure(1, String.format("Failed to delete %s %s", d.getPluginId(), d.getVersion())); + /* Find the ExtensionPoint */ + protected Class getExtensionPoint(Class clazz) { + Class superClass = clazz.getSuperclass(); + if (ExtensionPoint.class.isAssignableFrom(superClass)) { + return superClass; } + return getExtensionPoint(superClass); } } - @CommandMetaData(name = "install", description = "Download and installs a plugin", hidden = true) - public static class InstallPlugin extends SshCommand { - - @Argument(index = 0, required = true, metaVar = "||", usage = "the id, name, or the url of the plugin to download and install") - protected String urlOrIdOrName; - - @Option(name = "--version", usage = "The specific version to install") - private String version; - + @CommandMetaData(name = "refresh", description = "Refresh the plugin registry data") + public static class RefreshPlugins extends SshCommand { @Override - public void run() throws UnloggedFailure { + public void run() throws Failure { IGitblit gitblit = getContext().getGitblit(); - try { - String ulc = urlOrIdOrName.toLowerCase(); - if (ulc.startsWith("http://") || ulc.startsWith("https://")) { - if (gitblit.installPlugin(urlOrIdOrName)) { - stdout.println(String.format("Installed %s", urlOrIdOrName)); - } else { - new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName)); - } - } else { - PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version); - if (pv == null) { - throw new UnloggedFailure(1, String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName)); - } - if (gitblit.installPlugin(pv)) { - stdout.println(String.format("Installed %s", urlOrIdOrName)); - } else { - throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName)); - } - } - } catch (Exception e) { - log.error("Failed to install " + urlOrIdOrName, e); - throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName), e); - } + gitblit.refreshRegistry(); } } @@ -331,13 +423,22 @@ public class PluginDispatcher extends DispatchCommand { @Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry") protected boolean refresh; + @Option(name = "--updates", aliases = { "-u" }, usage = "show available updates") + protected boolean updates; + @Override protected List getItems() throws UnloggedFailure { IGitblit gitblit = getContext().getGitblit(); if (refresh) { gitblit.refreshRegistry(); } - List list = gitblit.getRegisteredPlugins(); + + List list; + if (updates) { + list = gitblit.getRegisteredPlugins(InstallState.CAN_UPDATE); + } else { + list = gitblit.getRegisteredPlugins(); + } return list; } @@ -350,19 +451,20 @@ public class PluginDispatcher extends DispatchCommand { protected void asTable(List list) { String[] headers; if (verbose) { - String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" }; + String [] h = { "Id", "Name", "Description", "Installed", "Current", "Requires", "State", "Registry" }; headers = h; } else { - String [] h = { "Name", "Description", "Installed", "Release", "State" }; + String [] h = { "Id", "Name", "Installed", "Current", "Requires", "State" }; headers = h; } Object[][] data = new Object[list.size()][]; for (int i = 0; i < list.size(); i++) { PluginRegistration p = list.get(i); + PluginRelease curr = p.getCurrentRelease(); if (verbose) { - data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider}; + data[i] = new Object[] {p.id, p.name, p.description, p.installedRelease, curr.version, curr.requires, p.getInstallState(), p.registry}; } else { - data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()}; + data[i] = new Object[] {p.id, p.name, p.installedRelease, curr.version, curr.requires, p.getInstallState()}; } } @@ -372,12 +474,76 @@ public class PluginDispatcher extends DispatchCommand { @Override protected void asTabbed(List list) { for (PluginRegistration p : list) { + PluginRelease curr = p.getCurrentRelease(); if (verbose) { - outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider); + outTabbed(p.id, p.name, p.description, p.installedRelease, curr.version, curr.requires, p.getInstallState(), p.provider, p.registry); } else { - outTabbed(p.name, p.description, p.currentRelease, p.getInstallState()); + outTabbed(p.id, p.name, p.installedRelease, curr.version, curr.requires, p.getInstallState()); } } } } + + @CommandMetaData(name = "install", description = "Download and installs a plugin") + public static class InstallPlugin extends SshCommand { + + @Argument(index = 0, required = true, metaVar = "||", usage = "the id, name, or the url of the plugin to download and install") + protected String urlOrIdOrName; + + @Option(name = "--version", usage = "The specific version to install") + private String version; + + @Option(name = "--noverify", usage = "Disable checksum verification") + private boolean disableChecksum; + + @Override + public void run() throws Failure { + IGitblit gitblit = getContext().getGitblit(); + try { + String ulc = urlOrIdOrName.toLowerCase(); + if (ulc.startsWith("http://") || ulc.startsWith("https://")) { + if (gitblit.installPlugin(urlOrIdOrName, !disableChecksum)) { + stdout.println(String.format("Installed %s", urlOrIdOrName)); + } else { + new Failure(1, String.format("Failed to install %s", urlOrIdOrName)); + } + } else { + PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version); + if (pv == null) { + throw new Failure(1, String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName)); + } + if (gitblit.installPlugin(pv.url, !disableChecksum)) { + stdout.println(String.format("Installed %s", urlOrIdOrName)); + } else { + throw new Failure(1, String.format("Failed to install %s", urlOrIdOrName)); + } + } + } catch (Exception e) { + log.error("Failed to install " + urlOrIdOrName, e); + throw new Failure(1, String.format("Failed to install %s", urlOrIdOrName), e); + } + } + } + + @CommandMetaData(name = "uninstall", aliases = { "rm", "del" }, description = "Uninstall a plugin") + public static class UninstallPlugin extends PluginCommand { + + @Argument(index = 0, required = true, metaVar = "", usage = "the plugin to uninstall") + protected String id; + + @Override + public void run() throws Failure { + IGitblit gitblit = getContext().getGitblit(); + PluginWrapper pluginWrapper = getPlugin(id); + if (pluginWrapper == null) { + throw new UnloggedFailure(String.format("Plugin %s is not installed!", id)); + } + + if (gitblit.deletePlugin(pluginWrapper.getPluginId())) { + stdout.println(String.format("Uninstalled %s", pluginWrapper.getPluginId())); + } else { + throw new Failure(1, String.format("Failed to uninstall %s", pluginWrapper.getPluginId())); + } + } + } } diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java index 3c378669..bebb4ac9 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java @@ -20,6 +20,8 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ro.fortsoft.pf4j.PluginWrapper; + import com.gitblit.manager.IGitblit; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.SshDaemonClient; @@ -49,9 +51,10 @@ class RootDispatcher extends DispatchCommand { List exts = gitblit.getExtensions(DispatchCommand.class); for (DispatchCommand ext : exts) { Class extClass = ext.getClass(); - String plugin = gitblit.whichPlugin(extClass).getDescriptor().getPluginId(); + PluginWrapper wrapper = gitblit.whichPlugin(extClass); + String plugin = wrapper.getDescriptor().getPluginId(); CommandMetaData meta = extClass.getAnnotation(CommandMetaData.class); - log.info("Dispatcher {} is loaded from plugin {}", meta.name(), plugin); + log.debug("Dispatcher {} is loaded from plugin {}", meta.name(), plugin); register(user, ext); } } diff --git a/src/main/java/com/gitblit/utils/StringUtils.java b/src/main/java/com/gitblit/utils/StringUtils.java index 5813c3ae..7605fe01 100644 --- a/src/main/java/com/gitblit/utils/StringUtils.java +++ b/src/main/java/com/gitblit/utils/StringUtils.java @@ -307,7 +307,7 @@ public class StringUtils { * @param bytes * @return byte array as hex string */ - private static String toHex(byte[] bytes) { + public static String toHex(byte[] bytes) { StringBuilder sb = new StringBuilder(bytes.length * 2); for (int i = 0; i < bytes.length; i++) { if ((bytes[i] & 0xff) < 0x10) { -- cgit v1.2.3 From e5d6095f0d804cb62d00fbb164f7c49371d412d6 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 11 Apr 2014 13:45:51 -0400 Subject: Add missing unit test utility class --- .../tests/JschConfigTestSessionFactory.java | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/test/java/com/gitblit/tests/JschConfigTestSessionFactory.java (limited to 'src') diff --git a/src/test/java/com/gitblit/tests/JschConfigTestSessionFactory.java b/src/test/java/com/gitblit/tests/JschConfigTestSessionFactory.java new file mode 100644 index 00000000..5d24b401 --- /dev/null +++ b/src/test/java/com/gitblit/tests/JschConfigTestSessionFactory.java @@ -0,0 +1,33 @@ +package com.gitblit.tests; + +import java.security.KeyPair; + +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.util.FS; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; + +public class JschConfigTestSessionFactory extends JschConfigSessionFactory { + + final KeyPair keyPair; + + public JschConfigTestSessionFactory(KeyPair keyPair) { + this.keyPair = keyPair; + } + + @Override + protected void configure(OpenSshConfig.Host host, Session session) { + session.setConfig("StrictHostKeyChecking", "no"); + } + + @Override + protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { + JSch jsch = super.getJSch(hc, fs); +// jsch.removeAllIdentity(); +// jsch.addIdentity("unittest", keyPair.getPrivate().getEncoded(), keyPair.getPublic().getEncoded(), null); + return jsch; + } +} \ No newline at end of file -- cgit v1.2.3 From ec53f7c3792372bfeb449126594076d1bcf5f084 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 11 Apr 2014 13:40:22 -0400 Subject: Ensure plugins dir is created before pf4j is instantiated --- src/main/java/com/gitblit/manager/PluginManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java index 9cefc88d..1c26fa15 100644 --- a/src/main/java/com/gitblit/manager/PluginManager.java +++ b/src/main/java/com/gitblit/manager/PluginManager.java @@ -80,6 +80,7 @@ public class PluginManager implements IPluginManager, PluginStateListener { public PluginManager(IRuntimeManager runtimeManager) { File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"); + dir.mkdirs(); this.runtimeManager = runtimeManager; this.pf4j = new DefaultPluginManager(dir); } @@ -229,11 +230,11 @@ public class PluginManager implements IPluginManager, PluginStateListener { } @Override - public synchronized boolean refreshRegistry() { + public synchronized boolean refreshRegistry(boolean verifyChecksum) { String dr = "http://gitblit.github.io/gitblit-registry/plugins.json"; String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr); try { - File file = download(url, true); + File file = download(url, verifyChecksum); if (file != null && file.exists()) { URL selfUrl = new URL(url.substring(0, url.lastIndexOf('/'))); // replace ${self} with the registry url @@ -260,7 +261,7 @@ public class PluginManager implements IPluginManager, PluginStateListener { File[] files = folder.listFiles(jsonFilter); if (files == null || files.length == 0) { // automatically retrieve the registry if we don't have a local copy - refreshRegistry(); + refreshRegistry(true); files = folder.listFiles(jsonFilter); } -- cgit v1.2.3 From e902064590962412b45f3d6ffccaece401f7f5c1 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 11 Apr 2014 13:41:14 -0400 Subject: Allow user to disable checksum verification on plugin registry refresh --- src/main/java/com/gitblit/manager/GitblitManager.java | 4 ++-- src/main/java/com/gitblit/manager/IPluginManager.java | 4 +++- .../com/gitblit/transport/ssh/commands/PluginDispatcher.java | 11 +++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java index 191d7cf1..5fca0c24 100644 --- a/src/main/java/com/gitblit/manager/GitblitManager.java +++ b/src/main/java/com/gitblit/manager/GitblitManager.java @@ -1250,8 +1250,8 @@ public class GitblitManager implements IGitblit { } @Override - public boolean refreshRegistry() { - return pluginManager.refreshRegistry(); + public boolean refreshRegistry(boolean verifyChecksum) { + return pluginManager.refreshRegistry(verifyChecksum); } @Override diff --git a/src/main/java/com/gitblit/manager/IPluginManager.java b/src/main/java/com/gitblit/manager/IPluginManager.java index 33763aa8..fd4247ed 100644 --- a/src/main/java/com/gitblit/manager/IPluginManager.java +++ b/src/main/java/com/gitblit/manager/IPluginManager.java @@ -118,8 +118,10 @@ public interface IPluginManager extends IManager { /** * Refresh the plugin registry. + * + * @param verifyChecksum */ - boolean refreshRegistry(); + boolean refreshRegistry(boolean verifyChecksum); /** * Install the plugin from the specified url. diff --git a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java index 99dd6d13..19cefe02 100644 --- a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java +++ b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java @@ -410,10 +410,14 @@ public class PluginDispatcher extends DispatchCommand { @CommandMetaData(name = "refresh", description = "Refresh the plugin registry data") public static class RefreshPlugins extends SshCommand { + + @Option(name = "--noverify", usage = "Disable checksum verification") + private boolean disableChecksum; + @Override public void run() throws Failure { IGitblit gitblit = getContext().getGitblit(); - gitblit.refreshRegistry(); + gitblit.refreshRegistry(!disableChecksum); } } @@ -426,11 +430,14 @@ public class PluginDispatcher extends DispatchCommand { @Option(name = "--updates", aliases = { "-u" }, usage = "show available updates") protected boolean updates; + @Option(name = "--noverify", usage = "Disable checksum verification") + private boolean disableChecksum; + @Override protected List getItems() throws UnloggedFailure { IGitblit gitblit = getContext().getGitblit(); if (refresh) { - gitblit.refreshRegistry(); + gitblit.refreshRegistry(!disableChecksum); } List list; -- cgit v1.2.3 From df3594165089d28409cdd57bbe5f3fde304557f1 Mon Sep 17 00:00:00 2001 From: James Moger Date: Fri, 11 Apr 2014 13:41:53 -0400 Subject: Improve plugin documentation --- build.xml | 16 +++++++++---- src/main/distrib/data/gitblit.properties | 25 +------------------ src/site/features.mkd | 5 ++-- src/site/setup_plugins.mkd | 41 +++++++++++++++++++++++++------- 4 files changed, 49 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/build.xml b/build.xml index 0baf090e..f9b171dc 100644 --- a/build.xml +++ b/build.xml @@ -558,8 +558,6 @@ - -

@@ -577,6 +575,10 @@
+ + + + @@ -620,6 +622,8 @@ + + @@ -877,8 +881,6 @@ - -
@@ -896,6 +898,10 @@ + + + + @@ -912,6 +918,8 @@ + + diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties index 3a531232..beeb965b 100644 --- a/src/main/distrib/data/gitblit.properties +++ b/src/main/distrib/data/gitblit.properties @@ -570,7 +570,7 @@ plugins.folder = ${baseFolder}/plugins # The registry of available plugins. # # SINCE 1.5.0 -plugins.registry = http://gitblit.github.io/gitblit-registry/plugins.json +plugins.registry = http://plugins.gitblit.com/plugins.json # # Groovy Integration @@ -1748,12 +1748,6 @@ realm.redmine.url = http://example.com/redmine # BASEFOLDER server.tempFolder = ${baseFolder}/temp -# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used. -# -# SINCE 0.5.0 -# RESTART REQUIRED -server.useNio = true - # Specify the maximum number of concurrent http/https worker threads to allow. # # SINCE 1.3.0 @@ -1783,14 +1777,6 @@ server.httpPort = 0 # RESTART REQUIRED server.httpsPort = 8443 -# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating -# Gitblit GO into an Apache HTTP server setup. <= 0 disables this connector. -# Recommended value: 8009 -# -# SINCE 0.9.0 -# RESTART REQUIRED -server.ajpPort = 0 - # Automatically redirect http requests to the secure https connector. # # This setting requires that you have configured server.httpPort and server.httpsPort. @@ -1819,15 +1805,6 @@ server.httpBindInterface = # RESTART REQUIRED server.httpsBindInterface = -# Specify the interface for Jetty to bind the AJP connector. -# You may specify an ip or an empty value to bind to all interfaces. -# Specifying localhost will result in Gitblit ONLY listening to requests to -# localhost. -# -# SINCE 0.9.0 -# RESTART REQUIRED -server.ajpBindInterface = localhost - # Alias of certificate to use for https/SSL serving. If blank the first # certificate found in the keystore will be used. # diff --git a/src/site/features.mkd b/src/site/features.mkd index 6d933cc1..dc048804 100644 --- a/src/site/features.mkd +++ b/src/site/features.mkd @@ -21,9 +21,10 @@ - **RWD** (clone and push with ref creation, deletion) - **RW+** (clone and push with ref creation, deletion, rewind) - Menu driven native platform clone links for all popular Git clients -- *Experimental* built-in Garbage Collection +- Garbage Collection service - Ability to federate with one or more other Gitblit instances - RSS/JSON RPC interface +- An evolving plugin infrastructure - Java/Swing Gitblit Manager tool - Responsive web UI that subtracts elements to be usable on phones, tablets, and desktop browsers - Groovy pre- and post- push hook scripts, per-repository or globally for all repositories @@ -46,7 +47,7 @@ - User-tracked reflog for pushes, tags, etc. - Fanout PubSub notifications service for self-hosted [Sparkleshare](http://sparkleshare.org) use - gh-pages display support (Jekyll is not supported) -- Branch metrics (uses Google Charts) +- Branch metrics - HEAD and Branch RSS feeds - Blame annotations view - Dates can optionally be displayed using the browser's reported timezone diff --git a/src/site/setup_plugins.mkd b/src/site/setup_plugins.mkd index 6ab4f1ef..b609a683 100644 --- a/src/site/setup_plugins.mkd +++ b/src/site/setup_plugins.mkd @@ -3,7 +3,7 @@ *SINCE 1.5.0* -Gitblit supports extending and enhacing the core functionality through plugins. This mechanism is very young and incomplete with few extension points, but you can expect it to evolve rapidly in upcoming releases. +Gitblit supports extending and enhancing the core functionality through plugins. This mechanism is very young and incomplete with few extension points, but you can expect it to evolve rapidly in upcoming releases. ### Architecture @@ -13,20 +13,45 @@ The zip plugins are stored in `${baseFolder}/plugins` and are unpacked on startu A plugin defines it's metadata in the META-INF/MANIFEST.MF file: - Plugin-Class: com.gitblit.plugins.cookbook.CookbookPlugin - Plugin-Dependencies: foo, bar - Plugin-Id: gitblit-plugin - Plugin-Provider: John Doe - Plugin-Version: 1.0 + Plugin-Class: com.gitblit.plugin.powertools.Powertools + Plugin-Dependencies: + Plugin-Id: com.gitblit.plugin:powertools + Plugin-Provider: James Moger + Plugin-Version: 1.1.0 In addition to extending Gitblit core, plugins can also define extension points that may be implemented by other plugins. Therefore a plugin may depend on other plugins. Plugin-Dependencies: foo, bar -Plugins are controlled by the `plugin` SSH dispatch command. Only *administrators* have permission to use this dispatch command. - +**NOTE:** The pf4j plugin framework relies on a javac apt processor to generate compile-time extension information, so be sure to enable apt processing in your build process. +### Managing Plugins + +Administrators may manage plugins through the `plugin` SSH dispatch command: + + ssh host plugin + +Through this command interface plugins can be started, stopped, disabled, enabled, installed, uninstalled, listed, etc. + +### Default Plugin Registry + +Gitblit provides a simple default registry of plugins. The registry is a JSON file and it lists plugin metadata and download locations. + + plugins.registry = http://plugins.gitblit.com/plugins.json + +The [registry](http://plugins.gitblit.com/plugins.json) is currently hosted in a [Git repository on Github](https://github.com/gitblit/gitblit-registry). This git repository is also a [Maven-compatible repository](http://plugins.gitblit.com), which hosts some plugin binaries. + +### Contributing Plugins to the Default Registry + +If you develop your own plugins that you want hosted by or linked in the default registry, open pull request for the registry repository. Any contributed binaries hosted in this repository must have Maven metadata and the SHA-1 & MD5 checksums. By default, Gitblit enforces checksum validation on all downloads. + +### Hosting your Own Registry / Allowing Multiple Registries + +The `plugins.json` file is parameterized with the `${self}` placeholder. This parameter is substituted on download with with the source URL of the registry file. This allows you to clone and serve your own copy of this git repository or just server your own `plugins.json` on your own network. + +Gitblit also supports loading multiple plugin registries. Just place another **properly formatted** `.json` file in `${baseFolder}/plugins` and Gitblit will load that as an additional registry. + ### Extension Point: SSH DispatchCommand You can provide your own custom SSH commands by extending the DispatchCommand. -- cgit v1.2.3