summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2014-04-11 12:51:50 -0600
committerJames Moger <james.moger@gitblit.com>2014-04-11 12:51:50 -0600
commit436bd3f0ecdee282c503a9eb0f7a240b7a68ff49 (patch)
tree555f9fb8bf77d1dfc6f0e9f8783fa17822b18b05
parent1f4cc634ec72934d5f71131e91cf0ff8cdf7d9e5 (diff)
parentdf3594165089d28409cdd57bbe5f3fde304557f1 (diff)
downloadgitblit-436bd3f0ecdee282c503a9eb0f7a240b7a68ff49.tar.gz
gitblit-436bd3f0ecdee282c503a9eb0f7a240b7a68ff49.zip
Merged #6 "Support serving repositories over the SSH transport"
-rw-r--r--.classpath3
-rw-r--r--NOTICE705
-rw-r--r--build.moxie5
-rw-r--r--build.xml68
-rw-r--r--gitblit.iml33
-rw-r--r--releases.moxie16
-rw-r--r--src/main/distrib/data/clientapps.json2
-rw-r--r--src/main/distrib/data/gitblit.properties89
-rw-r--r--src/main/java/WEB-INF/web.xml3
-rw-r--r--src/main/java/com/gitblit/Constants.java23
-rw-r--r--src/main/java/com/gitblit/DaggerModule.java46
-rw-r--r--src/main/java/com/gitblit/FederationClient.java2
-rw-r--r--src/main/java/com/gitblit/GitBlit.java119
-rw-r--r--src/main/java/com/gitblit/GitBlitServer.java1408
-rw-r--r--src/main/java/com/gitblit/git/GitblitReceivePackFactory.java64
-rw-r--r--src/main/java/com/gitblit/git/GitblitUploadPackFactory.java10
-rw-r--r--src/main/java/com/gitblit/git/RepositoryResolver.java17
-rw-r--r--src/main/java/com/gitblit/manager/AuthenticationManager.java40
-rw-r--r--src/main/java/com/gitblit/manager/GitblitManager.java124
-rw-r--r--src/main/java/com/gitblit/manager/IAuthenticationManager.java10
-rw-r--r--src/main/java/com/gitblit/manager/IGitblit.java9
-rw-r--r--src/main/java/com/gitblit/manager/IPluginManager.java165
-rw-r--r--src/main/java/com/gitblit/manager/PluginManager.java507
-rw-r--r--src/main/java/com/gitblit/manager/RuntimeManager.java4
-rw-r--r--src/main/java/com/gitblit/manager/ServicesManager.java80
-rw-r--r--src/main/java/com/gitblit/models/PluginRegistry.java160
-rw-r--r--src/main/java/com/gitblit/models/RepositoryUrl.java3
-rw-r--r--src/main/java/com/gitblit/models/TicketModel.java13
-rw-r--r--src/main/java/com/gitblit/models/UserModel.java12
-rw-r--r--src/main/java/com/gitblit/service/LuceneService.java2
-rw-r--r--src/main/java/com/gitblit/servlet/GitblitContext.java6
-rw-r--r--src/main/java/com/gitblit/servlet/SparkleShareInviteServlet.java47
-rw-r--r--src/main/java/com/gitblit/transport/git/GitDaemon.java (renamed from src/main/java/com/gitblit/git/GitDaemon.java)5
-rw-r--r--src/main/java/com/gitblit/transport/git/GitDaemonClient.java (renamed from src/main/java/com/gitblit/git/GitDaemonClient.java)2
-rw-r--r--src/main/java/com/gitblit/transport/git/GitDaemonService.java (renamed from src/main/java/com/gitblit/git/GitDaemonService.java)2
-rw-r--r--src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java113
-rw-r--r--src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java26
-rw-r--r--src/main/java/com/gitblit/transport/ssh/FileKeyManager.java267
-rw-r--r--src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java102
-rw-r--r--src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java110
-rw-r--r--src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java27
-rw-r--r--src/main/java/com/gitblit/transport/ssh/NullKeyManager.java76
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshDaemon.java231
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java74
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshKey.java212
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshServerSession.java34
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java72
-rw-r--r--src/main/java/com/gitblit/transport/ssh/UsernamePasswordAuthenticator.java61
-rw-r--r--src/main/java/com/gitblit/transport/ssh/WelcomeShell.java212
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java557
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java34
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java415
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/ListCommand.java92
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/ListFilterCommand.java66
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java556
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java65
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java87
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/SshCommandContext.java44
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java277
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java32
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java31
-rw-r--r--src/main/java/com/gitblit/transport/ssh/commands/VersionCommand.java28
-rw-r--r--src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java114
-rw-r--r--src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java63
-rw-r--r--src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java71
-rw-r--r--src/main/java/com/gitblit/transport/ssh/git/Receive.java38
-rw-r--r--src/main/java/com/gitblit/transport/ssh/git/Upload.java38
-rw-r--r--src/main/java/com/gitblit/transport/ssh/keys/BaseKeyCommand.java67
-rw-r--r--src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java268
-rw-r--r--src/main/java/com/gitblit/utils/FlipTable.java231
-rw-r--r--src/main/java/com/gitblit/utils/IdGenerator.java91
-rw-r--r--src/main/java/com/gitblit/utils/StringUtils.java2
-rw-r--r--src/main/java/com/gitblit/utils/TaskInfoFactory.java19
-rw-r--r--src/main/java/com/gitblit/utils/WorkQueue.java346
-rw-r--r--src/main/java/com/gitblit/utils/cli/CmdLineParser.java433
-rw-r--r--src/main/java/com/gitblit/utils/cli/SubcommandHandler.java43
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.java9
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.properties2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketPage.java27
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java12
-rw-r--r--src/main/java/log4j.properties3
-rw-r--r--src/site/design.mkd2
-rw-r--r--src/site/faq.mkd6
-rw-r--r--src/site/features.mkd11
-rw-r--r--src/site/resources/6x12.dfontbin0 -> 89952 bytes
-rw-r--r--src/site/resources/6x13.dfontbin0 -> 132838 bytes
-rw-r--r--src/site/resources/7x13.dfontbin0 -> 120394 bytes
-rw-r--r--src/site/resources/7x14.dfontbin0 -> 89640 bytes
-rw-r--r--src/site/setup_plugins.mkd72
-rw-r--r--src/site/setup_transport_http.mkd (renamed from src/site/setup_client.mkd)6
-rw-r--r--src/site/setup_transport_ssh.mkd86
-rw-r--r--src/test/config/test-gitblit.properties2
-rw-r--r--src/test/java/com/gitblit/tests/GitBlitSuite.java20
-rw-r--r--src/test/java/com/gitblit/tests/JschConfigTestSessionFactory.java33
-rw-r--r--src/test/java/com/gitblit/tests/SshDaemonTest.java90
-rw-r--r--src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java115
-rw-r--r--src/test/java/com/gitblit/tests/SshUnitTest.java141
97 files changed, 8981 insertions, 1185 deletions
diff --git a/.classpath b/.classpath
index fd38d56a..7939536b 100644
--- a/.classpath
+++ b/.classpath
@@ -52,6 +52,8 @@
<classpathentry kind="lib" path="ext/bcprov-jdk15on-1.49.jar" sourcepath="ext/src/bcprov-jdk15on-1.49.jar" />
<classpathentry kind="lib" path="ext/bcmail-jdk15on-1.49.jar" sourcepath="ext/src/bcmail-jdk15on-1.49.jar" />
<classpathentry kind="lib" path="ext/bcpkix-jdk15on-1.49.jar" sourcepath="ext/src/bcpkix-jdk15on-1.49.jar" />
+ <classpathentry kind="lib" path="ext/sshd-core-0.10.1.jar" sourcepath="ext/src/sshd-core-0.10.1.jar" />
+ <classpathentry kind="lib" path="ext/mina-core-2.0.7.jar" sourcepath="ext/src/mina-core-2.0.7.jar" />
<classpathentry kind="lib" path="ext/rome-0.9.jar" sourcepath="ext/src/rome-0.9.jar" />
<classpathentry kind="lib" path="ext/jdom-1.0.jar" sourcepath="ext/src/jdom-1.0.jar" />
<classpathentry kind="lib" path="ext/gson-1.7.2.jar" sourcepath="ext/src/gson-1.7.2.jar" />
@@ -74,6 +76,7 @@
<classpathentry kind="lib" path="ext/args4j-2.0.26.jar" sourcepath="ext/src/args4j-2.0.26.jar" />
<classpathentry kind="lib" path="ext/jedis-2.3.1.jar" sourcepath="ext/src/jedis-2.3.1.jar" />
<classpathentry kind="lib" path="ext/commons-pool2-2.0.jar" sourcepath="ext/src/commons-pool2-2.0.jar" />
+ <classpathentry kind="lib" path="ext/pf4j-0.7.1.jar" sourcepath="ext/src/pf4j-0.7.1.jar" />
<classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
<classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
<classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
diff --git a/NOTICE b/NOTICE
index 27c01e0e..da61b201 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,345 +1,360 @@
-Gitblit
-Copyright 2011 gitblit.com
-
-This product includes software developed at
-The Apache Software Foundation (http://www.apache.org/).
-
-This is an aggregated NOTICE file for the projects included
-in this distribution or linked to by this distribution.
-
----------------------------------------------------------------------------
-Bootstrap
----------------------------------------------------------------------------
- Bootstrap, released under the
- Apache Software License, Version 2.0.
-
- http://twitter.github.com/bootstrap
-
----------------------------------------------------------------------------
-google-code-prettify
----------------------------------------------------------------------------
- google-code-prettify, released under the
- Apache Software License, Version 2.0.
-
- http://code.google.com/p/google-code-prettify
-
----------------------------------------------------------------------------
-Commons Daemon
----------------------------------------------------------------------------
- Commons Daemon, released under the
- Apache Software License, Version 2.0.
-
- http://commons.apache.org/daemon
-
----------------------------------------------------------------------------
-JGit
----------------------------------------------------------------------------
- JGit, released under the
- Eclipse Distribution License 1.0.
-
- http://eclipse.org/jgit
-
----------------------------------------------------------------------------
-Apache Wicket
----------------------------------------------------------------------------
- Apache Wicket, released under the
- Apache Software License, Version 2.0.
-
- http://wicket.apache.org
-
----------------------------------------------------------------------------
-Jetty
----------------------------------------------------------------------------
- Jetty, released under the
- Apache Software License, Version 2.0.
-
- http://eclipse.org/jetty
-
----------------------------------------------------------------------------
-Apache Lucene
----------------------------------------------------------------------------
- Apache Lucene, released under the
- Apache Software License, Version 2.0.
-
- http://lucene.apache.org
-
----------------------------------------------------------------------------
-Groovy
----------------------------------------------------------------------------
- Groovy, released under the
- Apache Software License, Version 2.0.
-
- http://groovy.codehaus.org
-
----------------------------------------------------------------------------
-SLF4J
----------------------------------------------------------------------------
- SLF4J, released under the
- MIT/X11 License.
-
- http://www.slf4j.org
-
----------------------------------------------------------------------------
-Log4j
----------------------------------------------------------------------------
- Log4j, released under the
- Apache Software License, Version 2.0.
-
- http://logging.apache.org/log4j
-
----------------------------------------------------------------------------
-BouncyCastle
----------------------------------------------------------------------------
- BouncyCastle, released under the
- MIT/X11 License.
-
- http://www.bouncycastle.org
-
----------------------------------------------------------------------------
-JSch
----------------------------------------------------------------------------
- JSch - Java Secure Channel, released under the
- BSD License.
-
- http://www.jcraft.com/jsch
-
----------------------------------------------------------------------------
-Rome
----------------------------------------------------------------------------
- Rome RSS and Atom Java Utilities, released under the
- Apache Software License, Version 1.1.
-
- http://rome.dev.java.net
-
----------------------------------------------------------------------------
-jdom
----------------------------------------------------------------------------
- jdom xml library, released under the
- Apache-style Software License.
-
- http://www.jdom.org
-
----------------------------------------------------------------------------
-google-gson
----------------------------------------------------------------------------
- google-gson, released under the
- Apache-style Software License.
-
- http://code.google.com/p/google-gson
-
----------------------------------------------------------------------------
-javamail
----------------------------------------------------------------------------
- javamail, released under multiple licenses
- CDDL-1.0, BSD, GPL-2.0, GNU-Classpath.
-
- http://kenai.com/projects/javamail
-
----------------------------------------------------------------------------
-JUnit
----------------------------------------------------------------------------
- JUnit, released under the
- Common Public License.
-
- http://junit.org
-
----------------------------------------------------------------------------
-Fancybox image viewer
----------------------------------------------------------------------------
- Fancybox image viewer, released under the
- MIT and GPL Licenses.
-
- http://fancybox.net
-
----------------------------------------------------------------------------
-FatCow Icons
----------------------------------------------------------------------------
- FatCow Icons, released under the
- Creative Commons CC-BY License.
-
- http://www.fatcow.com/free-icons
-
----------------------------------------------------------------------------
-Git logo
----------------------------------------------------------------------------
- Git logo, released under the
- Creative Commons CC-BY License.
-
- http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon
-
----------------------------------------------------------------------------
-Git logo
----------------------------------------------------------------------------
- Git logo, released under the
- Creative Commons Attribution 3.0 Unported License.
-
- http://git-scm.com/downloads/logos
-
----------------------------------------------------------------------------
-magnifying glass search icon
----------------------------------------------------------------------------
- magnifying glass search icon, released under the
- Creative Commons CC-BY License.
-
- http://gnome.org
-
----------------------------------------------------------------------------
-GLYHPICONS
----------------------------------------------------------------------------
- GLPYHICONS, released under the
- Creative Commons CC-BY License.
-
- http://glyphicons.com
-
----------------------------------------------------------------------------
-UnboundID
----------------------------------------------------------------------------
- UnboundID, released under the
- GNU LESSER GENERAL PUBLIC LICENSE.
-
- http://www.unboundid.com
-
----------------------------------------------------------------------------
-JCalendar
----------------------------------------------------------------------------
- JCalendar, released under the
- GNU LESSER GENERAL PUBLIC LICENSE.
-
- http://www.toedter.com/en/jcalendar
-
----------------------------------------------------------------------------
-Commons-Compress
----------------------------------------------------------------------------
- Commons-Compress, released under the
- Apache Software License, Version 2.0.
-
- http://commons.apache.org/compress
-
----------------------------------------------------------------------------
-XZ for Java
----------------------------------------------------------------------------
- XZ for Java, released under the
- Public Domain
-
- http://tukaani.org/xz/java.html
-
----------------------------------------------------------------------------
-Iconic
----------------------------------------------------------------------------
- Iconic, release under the
- Creative Commons Share Alike 3.0 License.
-
- http://somerandomdude.com/work/iconic
-
----------------------------------------------------------------------------
-AngularJS
----------------------------------------------------------------------------
- AngularJS, release under the
- MIT License.
-
- http://angularjs.org/
-
----------------------------------------------------------------------------
-FreeMarker
----------------------------------------------------------------------------
- FreeMarker, release under a
- modified BSD License. (http://www.freemarker.org/docs/app_license.html)
-
- http://www.freemarker.org/
-
----------------------------------------------------------------------------
-Waffle
----------------------------------------------------------------------------
- Waffle, release under the
- Eclipse Public License, version 1.0
-
- http://dblock.github.io/waffle
-
----------------------------------------------------------------------------
-JNA
----------------------------------------------------------------------------
- JNA, release under the
- Lesser GNU Public License, version 2.1
-
- https://github.com/twall/jna
-
----------------------------------------------------------------------------
-Guava
----------------------------------------------------------------------------
- Guava, release under the
- Apache License 2.0.
-
- https://code.google.com/p/guava-libraries
-
----------------------------------------------------------------------------
-libpam4j
----------------------------------------------------------------------------
- libpam4j, release under the
- MIT license.
-
- https://github.com/kohsuke/libpam4j
-
----------------------------------------------------------------------------
-commons-codec
----------------------------------------------------------------------------
- commons-codec, release under the
- Apache License 2.0.
-
- http://commons.apache.org/proper/commons-codec
-
----------------------------------------------------------------------------
-pegdown
----------------------------------------------------------------------------
- pegdown, release under the
- Apache License 2.0.
-
- https://github.com/sirthias/pegdown
-
----------------------------------------------------------------------------
-font-awesome
----------------------------------------------------------------------------
- font-awesome, release under the
- SIL OFL 1.1.
-
- https://github.com/FortAwesome/Font-Awesome
-
----------------------------------------------------------------------------
-AUI (excerpts)
----------------------------------------------------------------------------
- AUI, release under the
- Apache License 2.0
-
- https://bitbucket.org/atlassian/aui
-
----------------------------------------------------------------------------
-Jedis
----------------------------------------------------------------------------
- Jedis, release under the
- MIT license
-
- https://github.com/xetorthio/jedis
-
----------------------------------------------------------------------------
-args4j
----------------------------------------------------------------------------
- args4j, release under the
- Apache License 2.0
-
- http://args4j.kohsuke.org
-
----------------------------------------------------------------------------
-jQuery
----------------------------------------------------------------------------
- jQuery, release under the
- MIT License
-
- https://jquery.org
-
----------------------------------------------------------------------------
-flotr2
----------------------------------------------------------------------------
- flotr2, release under the
- BSD License
-
- http://humblesoftware.com/flotr2
- \ No newline at end of file
+Gitblit
+Copyright 2011 gitblit.com
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+This is an aggregated NOTICE file for the projects included
+in this distribution or linked to by this distribution.
+
+---------------------------------------------------------------------------
+Bootstrap
+---------------------------------------------------------------------------
+ Bootstrap, released under the
+ Apache Software License, Version 2.0.
+
+ http://twitter.github.com/bootstrap
+
+---------------------------------------------------------------------------
+google-code-prettify
+---------------------------------------------------------------------------
+ google-code-prettify, released under the
+ Apache Software License, Version 2.0.
+
+ http://code.google.com/p/google-code-prettify
+
+---------------------------------------------------------------------------
+Commons Daemon
+---------------------------------------------------------------------------
+ Commons Daemon, released under the
+ Apache Software License, Version 2.0.
+
+ http://commons.apache.org/daemon
+
+---------------------------------------------------------------------------
+JGit
+---------------------------------------------------------------------------
+ JGit, released under the
+ Eclipse Distribution License 1.0.
+
+ http://eclipse.org/jgit
+
+---------------------------------------------------------------------------
+Apache Wicket
+---------------------------------------------------------------------------
+ Apache Wicket, released under the
+ Apache Software License, Version 2.0.
+
+ http://wicket.apache.org
+
+---------------------------------------------------------------------------
+Jetty
+---------------------------------------------------------------------------
+ Jetty, released under the
+ Apache Software License, Version 2.0.
+
+ http://eclipse.org/jetty
+
+---------------------------------------------------------------------------
+Apache Lucene
+---------------------------------------------------------------------------
+ Apache Lucene, released under the
+ Apache Software License, Version 2.0.
+
+ http://lucene.apache.org
+
+---------------------------------------------------------------------------
+Groovy
+---------------------------------------------------------------------------
+ Groovy, released under the
+ Apache Software License, Version 2.0.
+
+ http://groovy.codehaus.org
+
+---------------------------------------------------------------------------
+SLF4J
+---------------------------------------------------------------------------
+ SLF4J, released under the
+ MIT/X11 License.
+
+ http://www.slf4j.org
+
+---------------------------------------------------------------------------
+Log4j
+---------------------------------------------------------------------------
+ Log4j, released under the
+ Apache Software License, Version 2.0.
+
+ http://logging.apache.org/log4j
+
+---------------------------------------------------------------------------
+BouncyCastle
+---------------------------------------------------------------------------
+ BouncyCastle, released under the
+ MIT/X11 License.
+
+ http://www.bouncycastle.org
+
+---------------------------------------------------------------------------
+JSch
+---------------------------------------------------------------------------
+ JSch - Java Secure Channel, released under the
+ BSD License.
+
+ http://www.jcraft.com/jsch
+
+---------------------------------------------------------------------------
+Rome
+---------------------------------------------------------------------------
+ Rome RSS and Atom Java Utilities, released under the
+ Apache Software License, Version 1.1.
+
+ http://rome.dev.java.net
+
+---------------------------------------------------------------------------
+jdom
+---------------------------------------------------------------------------
+ jdom xml library, released under the
+ Apache-style Software License.
+
+ http://www.jdom.org
+
+---------------------------------------------------------------------------
+google-gson
+---------------------------------------------------------------------------
+ google-gson, released under the
+ Apache-style Software License.
+
+ http://code.google.com/p/google-gson
+
+---------------------------------------------------------------------------
+javamail
+---------------------------------------------------------------------------
+ javamail, released under multiple licenses
+ CDDL-1.0, BSD, GPL-2.0, GNU-Classpath.
+
+ http://kenai.com/projects/javamail
+
+---------------------------------------------------------------------------
+JUnit
+---------------------------------------------------------------------------
+ JUnit, released under the
+ Common Public License.
+
+ http://junit.org
+
+---------------------------------------------------------------------------
+Fancybox image viewer
+---------------------------------------------------------------------------
+ Fancybox image viewer, released under the
+ MIT and GPL Licenses.
+
+ http://fancybox.net
+
+---------------------------------------------------------------------------
+FatCow Icons
+---------------------------------------------------------------------------
+ FatCow Icons, released under the
+ Creative Commons CC-BY License.
+
+ http://www.fatcow.com/free-icons
+
+---------------------------------------------------------------------------
+Git logo
+---------------------------------------------------------------------------
+ Git logo, released under the
+ Creative Commons CC-BY License.
+
+ http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon
+
+---------------------------------------------------------------------------
+Git logo
+---------------------------------------------------------------------------
+ Git logo, released under the
+ Creative Commons Attribution 3.0 Unported License.
+
+ http://git-scm.com/downloads/logos
+
+---------------------------------------------------------------------------
+magnifying glass search icon
+---------------------------------------------------------------------------
+ magnifying glass search icon, released under the
+ Creative Commons CC-BY License.
+
+ http://gnome.org
+
+---------------------------------------------------------------------------
+GLYHPICONS
+---------------------------------------------------------------------------
+ GLPYHICONS, released under the
+ Creative Commons CC-BY License.
+
+ http://glyphicons.com
+
+---------------------------------------------------------------------------
+UnboundID
+---------------------------------------------------------------------------
+ UnboundID, released under the
+ GNU LESSER GENERAL PUBLIC LICENSE.
+
+ http://www.unboundid.com
+
+---------------------------------------------------------------------------
+JCalendar
+---------------------------------------------------------------------------
+ JCalendar, released under the
+ GNU LESSER GENERAL PUBLIC LICENSE.
+
+ http://www.toedter.com/en/jcalendar
+
+---------------------------------------------------------------------------
+Commons-Compress
+---------------------------------------------------------------------------
+ Commons-Compress, released under the
+ Apache Software License, Version 2.0.
+
+ http://commons.apache.org/compress
+
+---------------------------------------------------------------------------
+XZ for Java
+---------------------------------------------------------------------------
+ XZ for Java, released under the
+ Public Domain
+
+ http://tukaani.org/xz/java.html
+
+---------------------------------------------------------------------------
+Iconic
+---------------------------------------------------------------------------
+ Iconic, release under the
+ Creative Commons Share Alike 3.0 License.
+
+ http://somerandomdude.com/work/iconic
+
+---------------------------------------------------------------------------
+AngularJS
+---------------------------------------------------------------------------
+ AngularJS, release under the
+ MIT License.
+
+ http://angularjs.org/
+
+---------------------------------------------------------------------------
+FreeMarker
+---------------------------------------------------------------------------
+ FreeMarker, release under a
+ modified BSD License. (http://www.freemarker.org/docs/app_license.html)
+
+ http://www.freemarker.org/
+
+---------------------------------------------------------------------------
+Waffle
+---------------------------------------------------------------------------
+ Waffle, release under the
+ Eclipse Public License, version 1.0
+
+ http://dblock.github.io/waffle
+
+---------------------------------------------------------------------------
+JNA
+---------------------------------------------------------------------------
+ JNA, release under the
+ Lesser GNU Public License, version 2.1
+
+ https://github.com/twall/jna
+
+---------------------------------------------------------------------------
+Guava
+---------------------------------------------------------------------------
+ Guava, release under the
+ Apache License 2.0.
+
+ https://code.google.com/p/guava-libraries
+
+---------------------------------------------------------------------------
+libpam4j
+---------------------------------------------------------------------------
+ libpam4j, release under the
+ MIT license.
+
+ https://github.com/kohsuke/libpam4j
+
+---------------------------------------------------------------------------
+commons-codec
+---------------------------------------------------------------------------
+ commons-codec, release under the
+ Apache License 2.0.
+
+ http://commons.apache.org/proper/commons-codec
+
+---------------------------------------------------------------------------
+pegdown
+---------------------------------------------------------------------------
+ pegdown, release under the
+ Apache License 2.0.
+
+ https://github.com/sirthias/pegdown
+
+---------------------------------------------------------------------------
+font-awesome
+---------------------------------------------------------------------------
+ font-awesome, release under the
+ SIL OFL 1.1.
+
+ https://github.com/FortAwesome/Font-Awesome
+
+---------------------------------------------------------------------------
+AUI (excerpts)
+---------------------------------------------------------------------------
+ AUI, release under the
+ Apache License 2.0
+
+ https://bitbucket.org/atlassian/aui
+
+---------------------------------------------------------------------------
+Jedis
+---------------------------------------------------------------------------
+ Jedis, release under the
+ MIT license
+
+ https://github.com/xetorthio/jedis
+
+---------------------------------------------------------------------------
+args4j
+---------------------------------------------------------------------------
+ args4j, release under the
+ Apache License 2.0
+
+ http://args4j.kohsuke.org
+
+---------------------------------------------------------------------------
+jQuery
+---------------------------------------------------------------------------
+ jQuery, release under the
+ MIT License
+
+ https://jquery.org
+
+---------------------------------------------------------------------------
+flotr2
+---------------------------------------------------------------------------
+ flotr2, release under the
+ BSD License
+
+ http://humblesoftware.com/flotr2
+
+---------------------------------------------------------------------------
+Mina SSHD
+---------------------------------------------------------------------------
+ Mina SSHD, release under the
+ Apache License 2.0
+
+ https://mina.apache.org
+
+---------------------------------------------------------------------------
+pf4j
+---------------------------------------------------------------------------
+ pf4j, release under the
+ Apache License 2.0
+
+ https://github.com/decebals/pf4j
diff --git a/build.moxie b/build.moxie
index 19a9027f..87677373 100644
--- a/build.moxie
+++ b/build.moxie
@@ -109,6 +109,8 @@ properties: {
bouncycastle.version : 1.49
selenium.version : 2.28.0
wikitext.version : 1.4
+ sshd.version: 0.10.1
+ mina.version: 2.0.7
}
# Dependencies
@@ -155,6 +157,8 @@ dependencies:
- compile 'org.bouncycastle:bcprov-jdk15on:${bouncycastle.version}' :war :authority
- 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
@@ -170,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.7.1' :war
- test 'junit'
# Dependencies for Selenium web page testing
- test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
diff --git a/build.xml b/build.xml
index 32cb9f58..f9b171dc 100644
--- a/build.xml
+++ b/build.xml
@@ -561,19 +561,24 @@
<page name="Gitblit as a viewer" src="setup_viewer.mkd" />
</menu>
<divider />
- <menu name="Client Configuration" pager="true" pagerPlacement="bottom" pagerLayout="justified">
- <page name="git client setup" src="setup_client.mkd" />
- <page name="eclipse plugin" src="eclipse_plugin.mkd" />
+ <menu name="Client Usage" pager="true" pagerPlacement="bottom" pagerLayout="justified">
+ <page name="using HTTP/HTTPS" src="setup_transport_http.mkd" />
+ <page name="using SSH" src="setup_transport_ssh.mkd" />
+ <page name="using the Eclipse plugin" src="eclipse_plugin.mkd" />
</menu>
<divider />
- <menu name="Tickets" pager="true" pagerPlacement="bottom" pagerLayout="justified">
- <page name="overview" src="tickets_overview.mkd" />
- <page name="using" src="tickets_using.mkd" />
- <page name="barnum" src="tickets_barnum.mkd" />
+ <menu name="Tickets" pager="true" pagerPlacement="bottom" pagerLayout="justified">
+ <page name="overview" src="tickets_overview.mkd" />
+ <page name="using" src="tickets_using.mkd" />
+ <page name="barnum" src="tickets_barnum.mkd" />
<page name="setup" src="tickets_setup.mkd" />
- <page name="replication &amp; advanced administration" src="tickets_replication.mkd" />
- </menu>
- <divider />
+ <page name="replication &amp; advanced administration" src="tickets_replication.mkd" />
+ </menu>
+ <divider />
+ <menu name="Plugins" pager="true" pagerPlacement="bottom" pagerLayout="justified">
+ <page name="plugins" src="setup_plugins.mkd" />
+ </menu>
+ <divider />
<page name="federation" src="federation.mkd" />
<divider />
<page name="settings" src="properties.mkd" />
@@ -617,6 +622,8 @@
<divider />
<link name="Gitblit (Self-Hosted)" src="https://dev.gitblit.com" />
<divider />
+ <link name="Plugin Registry" src="http://plugins.gitblit.com" />
+ <divider />
<link name="Github" src="${project.scmUrl}" />
<link name="Issues" src="${project.issuesUrl}" />
<link name="Discussion" src="${project.forumUrl}" />
@@ -877,19 +884,24 @@
<page name="Gitblit as a viewer" src="setup_viewer.mkd" />
</menu>
<divider />
- <menu name="Client Configuration" pager="true" pagerPlacement="bottom" pagerLayout="justified">
- <page name="git client setup" src="setup_client.mkd" />
- <page name="eclipse plugin" src="eclipse_plugin.mkd" />
+ <menu name="Client Usage" pager="true" pagerPlacement="bottom" pagerLayout="justified">
+ <page name="using HTTP/HTTPS" src="setup_transport_http.mkd" />
+ <page name="using SSH" src="setup_transport_ssh.mkd" />
+ <page name="using the Eclipse plugin" src="eclipse_plugin.mkd" />
</menu>
<divider />
- <menu name="Tickets" pager="true" pagerPlacement="bottom" pagerLayout="justified">
- <page name="overview" src="tickets_overview.mkd" />
- <page name="using" src="tickets_using.mkd" />
- <page name="barnum" src="tickets_barnum.mkd" />
- <page name="setup" src="tickets_setup.mkd" />
+ <menu name="Tickets" pager="true" pagerPlacement="bottom" pagerLayout="justified">
+ <page name="overview" src="tickets_overview.mkd" />
+ <page name="using" src="tickets_using.mkd" />
+ <page name="barnum" src="tickets_barnum.mkd" />
+ <page name="setup" src="tickets_setup.mkd" />
<page name="replication &amp; advanced administration" src="tickets_replication.mkd" />
- </menu>
- <divider />
+ </menu>
+ <divider />
+ <menu name="Plugins" pager="true" pagerPlacement="bottom" pagerLayout="justified">
+ <page name="plugins" src="setup_plugins.mkd" />
+ </menu>
+ <divider />
<page name="federation" src="federation.mkd" />
<divider />
<page name="settings" src="properties.mkd" />
@@ -906,6 +918,8 @@
<link name="Gitblit Demo (RELEASE)" src="https://demo-gitblit.rhcloud.com" />
<link name="Gitbilt Next (SNAPSHOT)" src="https://next-gitblit.rhcloud.com" />
<divider />
+ <link name="Plugin Registry" src="http://plugins.gitblit.com" />
+ <divider />
<link name="Github" src="${project.scmUrl}" />
<link name="Issues" src="${project.issuesUrl}" />
<link name="Discussion" src="${project.forumUrl}" />
@@ -1040,5 +1054,19 @@
<arg value="-DcreateChecksum=true" />
</exec>
</target>
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Install Gitblit JAR for usage as Moxie artifact
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <target name="installMoxie" depends="compile" description="Install Gitblit JAR as a Moxie artifact">
+ <local name="project.jar" />
+ <property name="project.jar" value="${project.targetDirectory}/${project.artifactId}-${project.version}.jar" />
+ <property name="resourceFolderPrefix" value="" />
+ <mx:jar destfile="${project.jar}" includeresources="true" resourceFolderPrefix="${resourceFolderPrefix}" />
+
+ <mx:install />
+ </target>
</project>
diff --git a/gitblit.iml b/gitblit.iml
index 8e787cba..bd5df9b7 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -529,6 +529,28 @@
</library>
</orderEntry>
<orderEntry type="module-library">
+ <library name="sshd-core-0.10.1.jar">
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/ext/sshd-core-0.10.1.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES>
+ <root url="jar://$MODULE_DIR$/ext/src/sshd-core-0.10.1.jar!/" />
+ </SOURCES>
+ </library>
+ </orderEntry>
+ <orderEntry type="module-library">
+ <library name="mina-core-2.0.7.jar">
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/ext/mina-core-2.0.7.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES>
+ <root url="jar://$MODULE_DIR$/ext/src/mina-core-2.0.7.jar!/" />
+ </SOURCES>
+ </library>
+ </orderEntry>
+ <orderEntry type="module-library">
<library name="rome-0.9.jar">
<CLASSES>
<root url="jar://$MODULE_DIR$/ext/rome-0.9.jar!/" />
@@ -768,6 +790,17 @@
</SOURCES>
</library>
</orderEntry>
+ <orderEntry type="module-library">
+ <library name="pf4j-0.7.1.jar">
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/ext/pf4j-0.7.1.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES>
+ <root url="jar://$MODULE_DIR$/ext/src/pf4j-0.7.1.jar!/" />
+ </SOURCES>
+ </library>
+ </orderEntry>
<orderEntry type="module-library" scope="TEST">
<library name="junit-4.11.jar">
<CLASSES>
diff --git a/releases.moxie b/releases.moxie
index bba8de3f..e79782b6 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -23,20 +23,36 @@ 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
+ - Mina SSHD 0.10.1
+ - pf4j 0.7.1
contributors:
- James Moger
- David Ostrovsky
- Johann Ollivier-Lapeyre
- Jeremie Brebec
- Tim Ryan
+ - Decebal Suiu
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' }
+ - { name: 'git.sshKeysFolder', defaultValue: '${baseFolder}/ssh' }
+ - { 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/clientapps.json b/src/main/distrib/data/clientapps.json
index 31e53efd..a19cbcc8 100644
--- a/src/main/distrib/data/clientapps.json
+++ b/src/main/distrib/data/clientapps.json
@@ -82,7 +82,7 @@
"title": "SparkleShare\u2122",
"description": "an open source collaboration and sharing tool",
"legal": "released under the GPLv3 open source license",
- "cloneUrl": "sparkleshare://addProject/${baseUrl}/sparkleshare/${repoUrl}.xml",
+ "cloneUrl": "sparkleshare://addProject/${baseUrl}/sparkleshare/${username}@${repository}.xml",
"productUrl": "http://sparkleshare.org",
"transports": [ "ssh" ],
"platforms": [ "windows", "macintosh", "linux" ],
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index 3c605394..beeb965b 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -93,6 +93,50 @@ 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 =
+
+# 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
+
+# SSH backend NIO2|MINA.
+#
+# SINCE 1.5.0
+git.sshBackend = NIO2
+
+# 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
+
+
# 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://.
@@ -131,6 +175,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.
#
@@ -506,6 +560,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://plugins.gitblit.com/plugins.json
+
#
# Groovy Integration
#
@@ -1682,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
@@ -1717,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.
@@ -1753,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/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 @@
<url-pattern>/robots.txt</url-pattern>
</servlet-mapping>
-
<!-- Git Access Restriction Filter
<url-pattern> MUST match:
* GitServlet
@@ -322,4 +321,4 @@
<url-pattern>/*</url-pattern>
</filter-mapping>
-</web-app> \ No newline at end of file
+</web-app>
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 2a98b53f..af533996 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;
@@ -501,7 +503,7 @@ public class Constants {
}
public static enum AuthenticationType {
- CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
+ PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
public boolean isStandard() {
return ordinal() <= COOKIE.ordinal();
@@ -538,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/DaggerModule.java b/src/main/java/com/gitblit/DaggerModule.java
index 5ae8b253..e448867a 100644
--- a/src/main/java/com/gitblit/DaggerModule.java
+++ b/src/main/java/com/gitblit/DaggerModule.java
@@ -23,15 +23,22 @@ import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.INotificationManager;
+import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IProjectManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.manager.NotificationManager;
+import com.gitblit.manager.PluginManager;
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;
@@ -50,9 +57,11 @@ import dagger.Provides;
// core managers
IRuntimeManager.class,
+ IPluginManager.class,
INotificationManager.class,
IUserManager.class,
IAuthenticationManager.class,
+ IPublicKeyManager.class,
IRepositoryManager.class,
IProjectManager.class,
IFederationManager.class,
@@ -62,7 +71,7 @@ import dagger.Provides;
// the Gitblit Wicket app
GitBlitWebApp.class
- }
+ }
)
public class DaggerModule {
@@ -74,6 +83,10 @@ public class DaggerModule {
return new RuntimeManager(settings);
}
+ @Provides @Singleton IPluginManager providePluginManager(IRuntimeManager runtimeManager) {
+ return new PluginManager(runtimeManager);
+ }
+
@Provides @Singleton INotificationManager provideNotificationManager(IStoredSettings settings) {
return new NotificationManager(settings);
}
@@ -91,6 +104,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) {
@@ -124,18 +162,22 @@ public class DaggerModule {
@Provides @Singleton IGitblit provideGitblit(
IRuntimeManager runtimeManager,
+ IPluginManager pluginManager,
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
+ IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager) {
return new GitBlit(
runtimeManager,
+ pluginManager,
notificationManager,
userManager,
authenticationManager,
+ publicKeyManager,
repositoryManager,
projectManager,
federationManager);
@@ -146,6 +188,7 @@ public class DaggerModule {
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
+ IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager,
@@ -156,6 +199,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..c3dcd9da 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, null, 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 bbc8bd37..08342521 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -17,17 +17,23 @@ 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;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.INotificationManager;
+import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IProjectManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
@@ -41,6 +47,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;
@@ -64,17 +71,21 @@ public class GitBlit extends GitblitManager {
public GitBlit(
IRuntimeManager runtimeManager,
+ IPluginManager pluginManager,
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
+ IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager) {
super(runtimeManager,
+ pluginManager,
notificationManager,
userManager,
authenticationManager,
+ publicKeyManager,
repositoryManager,
projectManager,
federationManager);
@@ -101,10 +112,41 @@ public class GitBlit extends GitblitManager {
return this;
}
+ @Override
+ public boolean isServingRepositories() {
+ return servicesManager.isServingRepositories();
+ }
+
protected Object [] getModules() {
return new Object [] { new GitBlitModule()};
}
+ protected boolean acceptPush(Transport byTransport) {
+ if (byTransport == null) {
+ logger.info("Unknown transport, push rejected!");
+ return false;
+ }
+
+ Set<Transport> transports = new HashSet<Transport>();
+ 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.
*
@@ -121,19 +163,46 @@ public class GitBlit extends GitblitManager {
String username = StringUtils.encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username);
List<RepositoryUrl> list = new ArrayList<RepositoryUrl>();
+
// http/https url
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));
}
}
+ // ssh daemon url
+ String sshDaemonUrl = servicesManager.getSshDaemonUrl(request, user, repository);
+ 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));
+ }
+ }
+
// git daemon url
String gitDaemonUrl = servicesManager.getGitDaemonUrl(request, user, repository);
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));
}
}
@@ -152,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<RepositoryUrl>() {
+
+ @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;
}
@@ -175,6 +272,24 @@ public class GitBlit extends GitblitManager {
}
/**
+ * Delete the user and all associated public ssh keys.
+ */
+ @Override
+ public boolean deleteUser(String username) {
+ UserModel user = userManager.getUserModel(username);
+ return deleteUserModel(user);
+ }
+
+ @Override
+ public boolean deleteUserModel(UserModel model) {
+ boolean success = userManager.deleteUserModel(model);
+ if (success) {
+ getPublicKeyManager().removeAllKeys(model.username);
+ }
+ return success;
+ }
+
+ /**
* Delete the repository and all associated tickets.
*/
@Override
@@ -187,7 +302,7 @@ public class GitBlit extends GitblitManager {
public boolean deleteRepositoryModel(RepositoryModel model) {
boolean success = repositoryManager.deleteRepositoryModel(model);
if (success && ticketService != null) {
- return ticketService.deleteAll(model);
+ ticketService.deleteAll(model);
}
return success;
}
@@ -252,7 +367,7 @@ public class GitBlit extends GitblitManager {
FileTicketService.class,
BranchTicketService.class,
RedisTicketService.class
- }
+ }
)
class GitBlitModule {
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<String> filtered = new ArrayList<String>();
- 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<Connector> connectors = new ArrayList<Connector>();
-
- // 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<String> filtered = new ArrayList<String>();
+ 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<Connector> connectors = new ArrayList<Connector>();
+
+ // 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/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
index 7976fe56..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,11 +29,14 @@ 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;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
+import com.gitblit.transport.git.GitDaemonClient;
+import com.gitblit.transport.ssh.SshDaemonClient;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
@@ -64,22 +70,30 @@ public class GitblitReceivePackFactory<X> implements ReceivePackFactory<X> {
String origin = "";
String gitblitUrl = "";
int timeout = 0;
+ Transport transport = null;
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) {
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;
@@ -88,6 +102,20 @@ public class GitblitReceivePackFactory<X> implements ReceivePackFactory<X> {
// 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);
@@ -117,4 +145,30 @@ public class GitblitReceivePackFactory<X> implements ReceivePackFactory<X> {
return rp;
}
+
+ protected boolean acceptPush(Transport byTransport) {
+ if (byTransport == null) {
+ logger.info("Unknown transport, push rejected!");
+ return false;
+ }
+
+ Set<Transport> transports = new HashSet<Transport>();
+ 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/git/GitblitUploadPackFactory.java b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java
index d4e3ca15..7a476775 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
@@ -46,16 +47,9 @@ public class GitblitUploadPackFactory<X> implements UploadPackFactory<X> {
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
- user = authenticationManager.authenticate((HttpServletRequest) req);
- 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
diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java
index 208c1ae1..cc13144e 100644
--- a/src/main/java/com/gitblit/git/RepositoryResolver.java
+++ b/src/main/java/com/gitblit/git/RepositoryResolver.java
@@ -30,6 +30,8 @@ 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.SshDaemonClient;
/**
* Resolves repositories and grants export access.
@@ -67,6 +69,9 @@ public class RepositoryResolver<X> extends FileResolver<X> {
// git request
GitDaemonClient client = (GitDaemonClient) req;
client.setRepositoryName(name);
+ } else if (req instanceof SshDaemonClient) {
+ SshDaemonClient client = (SshDaemonClient) req;
+ client.setRepositoryName(name);
}
return repo;
}
@@ -91,13 +96,17 @@ public class RepositoryResolver<X> extends FileResolver<X> {
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 SshDaemonClient) {
+ // ssh is always authenticated
+ SshDaemonClient client = (SshDaemonClient) req;
+ user = client.getUser();
}
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..d1b1af0a 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.SshKey;
import com.gitblit.utils.Base64;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
@@ -159,7 +160,7 @@ public class AuthenticationManager implements IAuthenticationManager {
}
return this;
}
-
+
public void addAuthenticationProvider(AuthenticationProvider prov) {
authenticationProviders.add(prov);
}
@@ -290,6 +291,37 @@ public class AuthenticationManager implements IAuthenticationManager {
}
/**
+ * Authenticate a user based on a public key.
+ *
+ * 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(String username, SshKey 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 {1} public key",
+ user.username, key.getAlgorithm()));
+ return validateAuthentication(user, AuthenticationType.PUBLIC_KEY);
+ }
+ logger.warn(MessageFormat.format("Failed to find UserModel for {0} during public key authentication",
+ username));
+ }
+ } else {
+ logger.warn("Empty user passed to AuthenticationManager.authenticate!");
+ }
+ return null;
+ }
+
+
+ /**
* This method allows the authentication manager to reject authentication
* attempts. It is called after the username/secret have been verified to
* ensure that the authentication technique has been logged.
@@ -359,14 +391,14 @@ public class AuthenticationManager implements IAuthenticationManager {
}
}
}
-
+
// could not authenticate locally or with a provider
return null;
}
-
+
/**
* Returns a UserModel if local authentication succeeds.
- *
+ *
* @param user
* @param password
* @return a UserModel if local authentication succeeds, null otherwise
diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java
index b6c2b474..5fca0c24 100644
--- a/src/main/java/com/gitblit/manager/GitblitManager.java
+++ b/src/main/java/com/gitblit/manager/GitblitManager.java
@@ -42,6 +42,9 @@ import org.eclipse.jgit.transport.RefSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import ro.fortsoft.pf4j.PluginState;
+import ro.fortsoft.pf4j.PluginWrapper;
+
import com.gitblit.Constants;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.Constants.AccessRestrictionType;
@@ -57,6 +60,9 @@ 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;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
@@ -68,6 +74,8 @@ 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.transport.ssh.SshKey;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.JsonUtils;
@@ -100,12 +108,16 @@ public class GitblitManager implements IGitblit {
protected final IRuntimeManager runtimeManager;
+ protected final IPluginManager pluginManager;
+
protected final INotificationManager notificationManager;
protected final IUserManager userManager;
protected final IAuthenticationManager authenticationManager;
+ protected final IPublicKeyManager publicKeyManager;
+
protected final IRepositoryManager repositoryManager;
protected final IProjectManager projectManager;
@@ -114,18 +126,22 @@ public class GitblitManager implements IGitblit {
public GitblitManager(
IRuntimeManager runtimeManager,
+ IPluginManager pluginManager,
INotificationManager notificationManager,
IUserManager userManager,
IAuthenticationManager authenticationManager,
+ IPublicKeyManager publicKeyManager,
IRepositoryManager repositoryManager,
IProjectManager projectManager,
IFederationManager federationManager) {
this.settings = runtimeManager.getSettings();
this.runtimeManager = runtimeManager;
+ this.pluginManager = pluginManager;
this.notificationManager = notificationManager;
this.userManager = userManager;
this.authenticationManager = authenticationManager;
+ this.publicKeyManager = publicKeyManager;
this.repositoryManager = repositoryManager;
this.projectManager = projectManager;
this.federationManager = federationManager;
@@ -322,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!");
@@ -523,6 +542,11 @@ public class GitblitManager implements IGitblit {
throw new RuntimeException("This class does not have a ticket service!");
}
+ @Override
+ public IPublicKeyManager getPublicKeyManager() {
+ return publicKeyManager;
+ }
+
/*
* ISTOREDSETTINGS
*
@@ -651,6 +675,12 @@ public class GitblitManager implements IGitblit {
}
return user;
}
+
+ @Override
+ public UserModel authenticate(String username, SshKey key) {
+ return authenticationManager.authenticate(username, key);
+ }
+
@Override
public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) {
UserModel user = authenticationManager.authenticate(httpRequest, requiresCertificate);
@@ -1154,4 +1184,98 @@ public class GitblitManager implements IGitblit {
public boolean isIdle(Repository repository) {
return repositoryManager.isIdle(repository);
}
+
+ /*
+ * PLUGIN MANAGER
+ */
+
+ @Override
+ public void startPlugins() {
+ pluginManager.startPlugins();
+ }
+
+ @Override
+ public void stopPlugins() {
+ pluginManager.stopPlugins();
+ }
+
+ @Override
+ public List<PluginWrapper> getPlugins() {
+ return pluginManager.getPlugins();
+ }
+
+ @Override
+ public PluginWrapper getPlugin(String pluginId) {
+ return pluginManager.getPlugin(pluginId);
+ }
+
+ @Override
+ public List<Class<?>> getExtensionClasses(String pluginId) {
+ return pluginManager.getExtensionClasses(pluginId);
+ }
+
+ @Override
+ public <T> List<T> getExtensions(Class<T> clazz) {
+ return pluginManager.getExtensions(clazz);
+ }
+
+ @Override
+ public PluginWrapper whichPlugin(Class<?> clazz) {
+ return pluginManager.whichPlugin(clazz);
+ }
+
+ @Override
+ public PluginState startPlugin(String pluginId) {
+ return pluginManager.startPlugin(pluginId);
+ }
+
+ @Override
+ public PluginState stopPlugin(String pluginId) {
+ return pluginManager.stopPlugin(pluginId);
+ }
+
+ @Override
+ public boolean disablePlugin(String pluginId) {
+ return pluginManager.disablePlugin(pluginId);
+ }
+
+ @Override
+ public boolean enablePlugin(String pluginId) {
+ return pluginManager.enablePlugin(pluginId);
+ }
+
+ @Override
+ public boolean deletePlugin(String pluginId) {
+ return pluginManager.deletePlugin(pluginId);
+ }
+
+ @Override
+ public boolean refreshRegistry(boolean verifyChecksum) {
+ return pluginManager.refreshRegistry(verifyChecksum);
+ }
+
+ @Override
+ public boolean installPlugin(String url, boolean verifyChecksum) throws IOException {
+ return pluginManager.installPlugin(url, verifyChecksum);
+ }
+
+ @Override
+ public List<PluginRegistration> getRegisteredPlugins() {
+ return pluginManager.getRegisteredPlugins();
+ }
+
+ @Override
+ public List<PluginRegistration> getRegisteredPlugins(InstallState state) {
+ return pluginManager.getRegisteredPlugins(state);
+ }
+
+ @Override
+ public PluginRegistration lookupPlugin(String idOrName) {
+ return pluginManager.lookupPlugin(idOrName);
+ }
+
+ @Override
+ public PluginRelease lookupRelease(String idOrName, String version) {
+ return pluginManager.lookupRelease(idOrName, version);
+ }
}
diff --git a/src/main/java/com/gitblit/manager/IAuthenticationManager.java b/src/main/java/com/gitblit/manager/IAuthenticationManager.java
index 3007a303..33546d90 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.SshKey;
public interface IAuthenticationManager extends IManager {
@@ -34,6 +35,15 @@ public interface IAuthenticationManager extends IManager {
UserModel authenticate(HttpServletRequest httpRequest);
/**
+ * Authenticate a user based on a ssh public key.
+ *
+ * @param username
+ * @param key
+ * @return a user object or null
+ */
+ UserModel authenticate(String username, SshKey key);
+
+ /**
* Authenticate a user based on HTTP request parameters.
*
* Authentication by X509Certificate, servlet container principal, cookie,
diff --git a/src/main/java/com/gitblit/manager/IGitblit.java b/src/main/java/com/gitblit/manager/IGitblit.java
index 50210e9d..f3202c01 100644
--- a/src/main/java/com/gitblit/manager/IGitblit.java
+++ b/src/main/java/com/gitblit/manager/IGitblit.java
@@ -27,9 +27,11 @@ 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,
+ IPluginManager,
INotificationManager,
IUserManager,
IAuthenticationManager,
@@ -109,4 +111,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/manager/IPluginManager.java b/src/main/java/com/gitblit/manager/IPluginManager.java
new file mode 100644
index 00000000..fd4247ed
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/IPluginManager.java
@@ -0,0 +1,165 @@
+/*
+ * 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.manager;
+
+import java.io.IOException;
+import java.util.List;
+
+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 {
+
+ /**
+ * 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<Class<?>> getExtensionClasses(String pluginId);
+
+ /**
+ * Returns the list of extension instances for a given extension point.
+ *
+ * @param type
+ * @return a list of extension instances
+ */
+ <T> List<T> getExtensions(Class<T> type);
+
+ /**
+ * Returns the list of all resolved plugins.
+ *
+ * @return a list of resolved plugins
+ */
+ List<PluginWrapper> 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'.
+ *
+ * @param clazz extension point class to retrieve extension for
+ * @return PluginWrapper that loaded the given class
+ */
+ PluginWrapper whichPlugin(Class<?> clazz);
+
+ /**
+ * Disable the plugin represented by pluginId.
+ *
+ * @param pluginId
+ * @return true if successful
+ */
+ boolean disablePlugin(String pluginId);
+
+ /**
+ * Enable the plugin represented by pluginId.
+ *
+ * @param pluginId
+ * @return true if successful
+ */
+ boolean enablePlugin(String pluginId);
+
+ /**
+ * Delete the plugin represented by pluginId.
+ *
+ * @param pluginId
+ * @return true if successful
+ */
+ boolean deletePlugin(String pluginId);
+
+ /**
+ * Refresh the plugin registry.
+ *
+ * @param verifyChecksum
+ */
+ boolean refreshRegistry(boolean verifyChecksum);
+
+ /**
+ * 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.
+ *
+ * @return a list of registered plugins
+ */
+ List<PluginRegistration> 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<PluginRegistration> getRegisteredPlugins(InstallState state);
+
+ /**
+ * 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
new file mode 100644
index 00000000..1c26fa15
--- /dev/null
+++ b/src/main/java/com/gitblit/manager/PluginManager.java
@@ -0,0 +1,507 @@
+/*
+ * 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.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;
+
+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;
+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 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
+ private int connectTimeout = 20;
+
+ private int readTimeout = 12800;
+
+ public PluginManager(IRuntimeManager runtimeManager) {
+ File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+ dir.mkdirs();
+ this.runtimeManager = runtimeManager;
+ this.pf4j = new DefaultPluginManager(dir);
+ }
+
+ @Override
+ public void pluginStateChanged(PluginStateEvent event) {
+ logger.debug(event.toString());
+ }
+
+ @Override
+ public PluginManager start() {
+ pf4j.loadPlugins();
+ logger.debug("Starting plugins");
+ pf4j.startPlugins();
+ return this;
+ }
+
+ @Override
+ public PluginManager stop() {
+ logger.debug("Stopping plugins");
+ pf4j.stopPlugins();
+ return null;
+ }
+
+ /**
+ * Installs the plugin from the url.
+ *
+ * @param url
+ * @param verifyChecksum
+ * @return true if successful
+ */
+ @Override
+ 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;
+ }
+
+ String pluginId = pf4j.loadPlugin(file);
+ if (StringUtils.isEmpty(pluginId)) {
+ logger.error("Failed to load plugin {}", file);
+ return false;
+ }
+
+ 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 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<PluginWrapper> getPlugins() {
+ return pf4j.getPlugins();
+ }
+
+ @Override
+ public synchronized PluginWrapper getPlugin(String pluginId) {
+ return pf4j.getPlugin(pluginId);
+ }
+
+ @Override
+ public synchronized List<Class<?>> getExtensionClasses(String pluginId) {
+ List<Class<?>> list = new ArrayList<Class<?>>();
+ 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 synchronized <T> List<T> getExtensions(Class<T> type) {
+ return pf4j.getExtensions(type);
+ }
+
+ @Override
+ public synchronized PluginWrapper whichPlugin(Class<?> clazz) {
+ return pf4j.whichPlugin(clazz);
+ }
+
+ @Override
+ 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, verifyChecksum);
+ 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);
+ }
+ return false;
+ }
+
+ protected List<PluginRegistry> getRegistries() {
+ List<PluginRegistry> list = new ArrayList<PluginRegistry>();
+ 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(true);
+ 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);
+ registry.setup();
+ } catch (Exception e) {
+ logger.error("Failed to deserialize " + file, e);
+ }
+ if (registry != null) {
+ list.add(registry);
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public synchronized List<PluginRegistration> getRegisteredPlugins() {
+ List<PluginRegistration> list = new ArrayList<PluginRegistration>();
+ Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>();
+ for (PluginRegistry registry : getRegistries()) {
+ list.addAll(registry.registrations);
+ for (PluginRegistration reg : list) {
+ reg.installedRelease = null;
+ map.put(reg.id, reg);
+ }
+ }
+ for (PluginWrapper pw : pf4j.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 synchronized List<PluginRegistration> getRegisteredPlugins(InstallState state) {
+ List<PluginRegistration> list = getRegisteredPlugins();
+ Iterator<PluginRegistration> itr = list.iterator();
+ while (itr.hasNext()) {
+ if (state != itr.next().getInstallState()) {
+ itr.remove();
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public synchronized PluginRegistration lookupPlugin(String idOrName) {
+ for (PluginRegistration reg : getRegisteredPlugins()) {
+ if (reg.id.equalsIgnoreCase(idOrName) || reg.name.equalsIgnoreCase(idOrName)) {
+ return reg;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ 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;
+ }
+
+ /**
+ * Downloads a file with optional checksum verification.
+ *
+ * @param url
+ * @param verifyChecksum
+ * @return
+ * @throws IOException
+ */
+ protected File download(String url, boolean verifyChecksum) throws IOException {
+ File file = downloadFile(url);
+
+ File sha1File = null;
+ try {
+ sha1File = downloadFile(url + ".sha1");
+ } catch (IOException e) {
+ }
+
+ File md5File = null;
+ try {
+ md5File = downloadFile(url + ".md5");
+ } catch (IOException e) {
+
+ }
+
+ 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 the downloaded file
+ * @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");
+ 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<InputStream>() {
+ @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 destFile;
+ }
+
+ 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/manager/RuntimeManager.java b/src/main/java/com/gitblit/manager/RuntimeManager.java
index 45d1ea12..9805701b 100644
--- a/src/main/java/com/gitblit/manager/RuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -116,7 +116,9 @@ public class RuntimeManager implements IRuntimeManager {
*/
@Override
public boolean isServingRepositories() {
- return settings.getBoolean(Keys.git.enableGitServlet, true) || (settings.getInteger(Keys.git.daemonPort, 0) > 0);
+ return settings.getBoolean(Keys.git.enableGitServlet, true)
+ || (settings.getInteger(Keys.git.daemonPort, 0) > 0)
+ || (settings.getInteger(Keys.git.sshPort, 0) > 0);
}
/**
diff --git a/src/main/java/com/gitblit/manager/ServicesManager.java b/src/main/java/com/gitblit/manager/ServicesManager.java
index 8107a7d8..e0fc8bbd 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;
@@ -37,11 +38,13 @@ 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;
import com.gitblit.utils.TimeUtils;
@@ -67,6 +70,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 +82,7 @@ public class ServicesManager implements IManager {
configureFederation();
configureFanout();
configureGitDaemon();
+ configureSshDaemon();
return this;
}
@@ -90,9 +96,18 @@ public class ServicesManager implements IManager {
if (gitDaemon != null) {
gitDaemon.stop();
}
+ if (sshDaemon != null) {
+ sshDaemon.stop();
+ }
return this;
}
+ public boolean isServingRepositories() {
+ return settings.getBoolean(Keys.git.enableGitServlet, true)
+ || (gitDaemon != null && gitDaemon.isRunning())
+ || (sshDaemon != null && sshDaemon.isRunning());
+ }
+
protected void configureFederation() {
boolean validPassphrase = true;
String passphrase = settings.getString(Keys.federation.passphrase, "");
@@ -138,6 +153,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, new IdGenerator());
+ 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) {
@@ -177,8 +206,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;
}
}
@@ -204,6 +233,50 @@ public class ServicesManager implements IManager {
return AccessPermission.NONE;
}
+ public String getSshDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) {
+ if (user == null || UserModel.ANONYMOUS.equals(user)) {
+ // SSH always requires authentication - anonymous access prohibited
+ return null;
+ }
+ 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;
+ }
+
+
+ /**
+ * 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 {
@@ -225,6 +298,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/models/PluginRegistry.java b/src/main/java/com/gitblit/models/PluginRegistry.java
new file mode 100644
index 00000000..b5cf0ee1
--- /dev/null
+++ b/src/main/java/com/gitblit/models/PluginRegistry.java
@@ -0,0 +1,160 @@
+/*
+ * 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 java.util.concurrent.CopyOnWriteArrayList;
+
+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<PluginRegistration> registrations;
+
+ public PluginRegistry(String name) {
+ this.name = name;
+ registrations = new CopyOnWriteArrayList<PluginRegistration>();
+ }
+
+ public void setup() {
+ for (PluginRegistration reg : registrations) {
+ reg.registry = name;
+ }
+ }
+
+ 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 transient String registry;
+
+ public List<PluginRelease> releases;
+
+ public PluginRegistration(String id) {
+ this.id = id;
+ this.releases = new ArrayList<PluginRelease>();
+ }
+
+ 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)) {
+ 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 implements Comparable<PluginRelease> {
+ 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/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;
}
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<TicketModel> {
}
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<TicketModel> {
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/models/UserModel.java b/src/main/java/com/gitblit/models/UserModel.java
index 675835d3..64bca825 100644
--- a/src/main/java/com/gitblit/models/UserModel.java
+++ b/src/main/java/com/gitblit/models/UserModel.java
@@ -543,7 +543,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
// admins can create any repository
return true;
}
- if (canCreate) {
+ if (canCreate()) {
String projectPath = StringUtils.getFirstPathElement(repository);
if (!StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase(getPersonalPath())) {
// personal repository
@@ -552,6 +552,16 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
}
return false;
}
+
+ /**
+ * Returns true if the user is allowed to administer the specified repository
+ *
+ * @param repo
+ * @return true if the user can administer the repository
+ */
+ public boolean canAdmin(RepositoryModel repo) {
+ return canAdmin() || isMyPersonalRepository(repo.name);
+ }
public boolean isAuthenticated() {
return !UserModel.ANONYMOUS.equals(this) && isAuthenticated;
diff --git a/src/main/java/com/gitblit/service/LuceneService.java b/src/main/java/com/gitblit/service/LuceneService.java
index 714a1e29..868a2953 100644
--- a/src/main/java/com/gitblit/service/LuceneService.java
+++ b/src/main/java/com/gitblit/service/LuceneService.java
@@ -194,7 +194,7 @@ public class LuceneService implements Runnable {
* Synchronously indexes a repository. This may build a complete index of a
* repository or it may update an existing index.
*
- * @param name
+ * @param displayName
* the name of the repository
* @param repository
* the repository object
diff --git a/src/main/java/com/gitblit/servlet/GitblitContext.java b/src/main/java/com/gitblit/servlet/GitblitContext.java
index d4ec9671..553651da 100644
--- a/src/main/java/com/gitblit/servlet/GitblitContext.java
+++ b/src/main/java/com/gitblit/servlet/GitblitContext.java
@@ -43,10 +43,12 @@ import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.IManager;
import com.gitblit.manager.INotificationManager;
+import com.gitblit.manager.IPluginManager;
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 +151,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();
@@ -175,9 +177,11 @@ public class GitblitContext extends DaggerContext {
managers.add(runtime);
// start all other managers
+ startManager(injector, IPluginManager.class);
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/servlet/SparkleShareInviteServlet.java b/src/main/java/com/gitblit/servlet/SparkleShareInviteServlet.java
index d7f00c67..150dd68a 100644
--- a/src/main/java/com/gitblit/servlet/SparkleShareInviteServlet.java
+++ b/src/main/java/com/gitblit/servlet/SparkleShareInviteServlet.java
@@ -16,13 +16,13 @@
package com.gitblit.servlet;
import java.io.IOException;
+import java.net.URL;
import java.text.MessageFormat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import com.gitblit.Constants;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.dagger.DaggerServlet;
@@ -77,6 +77,12 @@ public class SparkleShareInviteServlet extends DaggerServlet {
javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
java.io.IOException {
+ int sshPort = settings.getInteger(Keys.git.sshPort, 0);
+ if (sshPort == 0) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.getWriter().append("SSH is not active on this server!");
+ return;
+ }
// extract repo name from request
String repoUrl = request.getPathInfo().substring(1);
@@ -85,25 +91,32 @@ public class SparkleShareInviteServlet extends DaggerServlet {
repoUrl = repoUrl.substring(0, repoUrl.length() - 4);
}
- String servletPath = Constants.R_PATH;
-
- int schemeIndex = repoUrl.indexOf("://") + 3;
- String host = repoUrl.substring(0, repoUrl.indexOf('/', schemeIndex));
- String path = repoUrl.substring(repoUrl.indexOf(servletPath) + servletPath.length());
String username = null;
+ String path;
int fetchIndex = repoUrl.indexOf('@');
if (fetchIndex > -1) {
- username = repoUrl.substring(schemeIndex, fetchIndex);
+ username = repoUrl.substring(0, fetchIndex);
+ path = repoUrl.substring(fetchIndex + 1);
+ } else {
+ path = repoUrl;
+ }
+
+ String host = request.getServerName();
+ String url = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
+ if (!StringUtils.isEmpty(url) && url.indexOf("localhost") == -1) {
+ host = new URL(url).getHost();
}
+
UserModel user;
if (StringUtils.isEmpty(username)) {
user = authenticationManager.authenticate(request);
} else {
user = userManager.getUserModel(username);
}
- if (user == null) {
- user = UserModel.ANONYMOUS;
- username = "";
+ if (user == null || user.disabled) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.getWriter().append("Access is not permitted!");
+ return;
}
// ensure that the requested repository exists
@@ -114,14 +127,20 @@ public class SparkleShareInviteServlet extends DaggerServlet {
return;
}
+ if (!user.canRewindRef(model)) {
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.getWriter().append(MessageFormat.format("{0} does not have RW+ permissions to \"{1}\"!", user.username, model.name));
+ }
+
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
sb.append("<sparkleshare><invite>\n");
- sb.append(MessageFormat.format("<address>{0}</address>\n", host));
- sb.append(MessageFormat.format("<remote_path>{0}{1}</remote_path>\n", servletPath, model.name));
- if (settings.getInteger(Keys.fanout.port, 0) > 0) {
+ sb.append(MessageFormat.format("<address>ssh://{0}@{1}:{2,number,0}/</address>\n", user.username, host, sshPort));
+ sb.append(MessageFormat.format("<remote_path>/{0}</remote_path>\n", model.name));
+ int fanoutPort = settings.getInteger(Keys.fanout.port, 0);
+ if (fanoutPort > 0) {
// Gitblit is running it's own fanout service for pubsub notifications
- sb.append(MessageFormat.format("<announcements_url>tcp://{0}:{1}</announcements_url>\n", request.getServerName(), settings.getString(Keys.fanout.port, "")));
+ sb.append(MessageFormat.format("<announcements_url>tcp://{0}:{1,number,0}</announcements_url>\n", request.getServerName(), fanoutPort));
}
sb.append("</invite></sparkleshare>\n");
diff --git a/src/main/java/com/gitblit/git/GitDaemon.java b/src/main/java/com/gitblit/transport/git/GitDaemon.java
index d026b5ed..6581ad87 100644
--- a/src/main/java/com/gitblit/git/GitDaemon.java
+++ b/src/main/java/com/gitblit/transport/git/GitDaemon.java
@@ -41,7 +41,7 @@
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-package com.gitblit.git;
+package com.gitblit.transport.git;
import java.io.IOException;
import java.io.InputStream;
@@ -69,6 +69,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.utils.StringUtils;
diff --git a/src/main/java/com/gitblit/git/GitDaemonClient.java b/src/main/java/com/gitblit/transport/git/GitDaemonClient.java
index 8d8cac6d..bc3d4cf7 100644
--- a/src/main/java/com/gitblit/git/GitDaemonClient.java
+++ b/src/main/java/com/gitblit/transport/git/GitDaemonClient.java
@@ -1,4 +1,4 @@
-package com.gitblit.git;
+package com.gitblit.transport.git;
/*
* Copyright (C) 2008-2009, Google Inc.
diff --git a/src/main/java/com/gitblit/git/GitDaemonService.java b/src/main/java/com/gitblit/transport/git/GitDaemonService.java
index 8dee7d0b..989b2b4c 100644
--- a/src/main/java/com/gitblit/git/GitDaemonService.java
+++ b/src/main/java/com/gitblit/transport/git/GitDaemonService.java
@@ -1,4 +1,4 @@
-package com.gitblit.git;
+package com.gitblit.transport.git;
/*
* Copyright (C) 2008-2009, Google Inc.
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..4ce26d0f
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/CachingPublicKeyAuthenticator.java
@@ -0,0 +1,113 @@
+/*
+ * 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 IPublicKeyManager keyManager;
+
+ protected final IAuthenticationManager authManager;
+
+ private final Map<ServerSession, Map<PublicKey, Boolean>> cache = new ConcurrentHashMap<ServerSession, Map<PublicKey, Boolean>>();
+
+ public CachingPublicKeyAuthenticator(IPublicKeyManager keyManager, IAuthenticationManager authManager) {
+ this.keyManager = keyManager;
+ this.authManager = authManager;
+ }
+
+ @Override
+ public boolean authenticate(String username, PublicKey key, ServerSession session) {
+ Map<PublicKey, Boolean> map = cache.get(session);
+ if (map == null) {
+ map = new HashMap<PublicKey, Boolean>();
+ 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<SshKey> keys = keyManager.getKeys(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) {
+ client.setUser(user);
+ client.setKey(key);
+ return true;
+ }
+ }
+ }
+
+ log.warn("could not authenticate {} for SSH using the supplied public key", username);
+ return false;
+ }
+
+ @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/DisabledFilesystemFactory.java b/src/main/java/com/gitblit/transport/ssh/DisabledFilesystemFactory.java
new file mode 100644
index 00000000..c0578f9d
--- /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.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.FileSystemView;
+import org.apache.sshd.common.file.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/FileKeyManager.java b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
new file mode 100644
index 00000000..a063dc7d
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/FileKeyManager.java
@@ -0,0 +1,267 @@
+/*
+ * 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.text.MessageFormat;
+import java.util.ArrayList;
+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;
+import com.google.common.base.Joiner;
+import com.google.common.io.Files;
+
+/**
+ * Manages public keys on the filesystem.
+ *
+ * @author James Moger
+ *
+ */
+public class FileKeyManager extends IPublicKeyManager {
+
+ protected final IRuntimeManager runtimeManager;
+
+ protected final Map<File, Long> lastModifieds;
+
+ public FileKeyManager(IRuntimeManager runtimeManager) {
+ this.runtimeManager = runtimeManager;
+ this.lastModifieds = new ConcurrentHashMap<File, Long>();
+ }
+
+ @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() {
+ log.info(toString());
+ return this;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public FileKeyManager stop() {
+ return this;
+ }
+
+ @Override
+ 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<SshKey> getKeysImpl(String username) {
+ try {
+ log.info("loading ssh keystore for {}", username);
+ File keystore = getKeystore(username);
+ if (!keystore.exists()) {
+ return null;
+ }
+ if (keystore.exists()) {
+ List<SshKey> list = new ArrayList<SshKey>();
+ for (String entry : Files.readLines(keystore, Charsets.ISO_8859_1)) {
+ if (entry.trim().length() == 0) {
+ // skip blanks
+ continue;
+ }
+ if (entry.charAt(0) == '#') {
+ // skip comments
+ continue;
+ }
+ 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()) {
+ return null;
+ }
+
+ lastModifieds.put(keystore, keystore.lastModified());
+ return list;
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot read ssh keys", e);
+ }
+ 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, SshKey key) {
+ try {
+ boolean replaced = false;
+ List<String> lines = new ArrayList<String>();
+ 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;
+ }
+
+ SshKey oldKey = parseKey(line);
+ if (key.equals(oldKey)) {
+ // replace key
+ lines.add(key.getPermission() + " " + key.getRawData());
+ replaced = true;
+ } else {
+ // retain key
+ lines.add(entry);
+ }
+ }
+ }
+
+ if (!replaced) {
+ // new key, append
+ lines.add(key.getPermission() + " " + key.getRawData());
+ }
+
+ // 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);
+ }
+ }
+
+ /**
+ * Removes the specified key from the keystore.
+ */
+ @Override
+ public boolean removeKey(String username, SshKey key) {
+ try {
+ File keystore = getKeystore(username);
+ if (keystore.exists()) {
+ List<String> lines = new ArrayList<String>();
+ 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 include keys that are NOT rmKey
+ SshKey oldKey = parseKey(line);
+ if (!key.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);
+ }
+
+ lastModifieds.remove(keystore);
+ keyCache.invalidate(username);
+ return true;
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot remove ssh key", e);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAllKeys(String username) {
+ File keystore = getKeystore(username);
+ if (keystore.delete()) {
+ lastModifieds.remove(keystore);
+ keyCache.invalidate(username);
+ return true;
+ }
+ 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;
+ }
+
+ 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/IPublicKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
new file mode 100644
index 00000000..1e74b2f0
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.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.text.MessageFormat;
+import java.util.Collections;
+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.CacheLoader.InvalidCacheLoadException;
+import com.google.common.cache.LoadingCache;
+
+/**
+ * Parent class for ssh public key managers.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class IPublicKeyManager implements IManager {
+
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ protected final LoadingCache<String, List<SshKey>> keyCache = CacheBuilder
+ .newBuilder().
+ expireAfterAccess(15, TimeUnit.MINUTES).
+ maximumSize(100)
+ .build(new CacheLoader<String, List<SshKey>>() {
+ @Override
+ public List<SshKey> load(String username) {
+ List<SshKey> keys = getKeysImpl(username);
+ if (keys == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(keys);
+ }
+ });
+
+ @Override
+ public abstract IPublicKeyManager start();
+
+ public abstract boolean isReady();
+
+ @Override
+ public abstract IPublicKeyManager stop();
+
+ public final List<SshKey> getKeys(String username) {
+ try {
+ if (isStale(username)) {
+ keyCache.invalidate(username);
+ }
+ return keyCache.get(username);
+ } catch (InvalidCacheLoadException e) {
+ if (e.getMessage() == null || !e.getMessage().contains("returned null")) {
+ log.error(MessageFormat.format("failed to retrieve keys for {0}", username), e);
+ }
+ } catch (ExecutionException e) {
+ log.error(MessageFormat.format("failed to retrieve keys for {0}", username), e);
+ }
+ return null;
+ }
+
+ public final void renameUser(String oldName, String newName) {
+ List<SshKey> 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<SshKey> getKeysImpl(String username);
+
+ public abstract boolean addKey(String username, SshKey key);
+
+ public abstract boolean removeKey(String username, SshKey key);
+
+ 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..357b34a2
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/MemoryKeyManager.java
@@ -0,0 +1,110 @@
+/*
+ * 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.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 {
+
+ final Map<String, List<SshKey>> keys;
+
+ public MemoryKeyManager() {
+ keys = new HashMap<String, List<SshKey>>();
+ }
+
+ @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) {
+ // always return true so we gets keys from our hashmap
+ return true;
+ }
+
+ @Override
+ protected List<SshKey> getKeysImpl(String username) {
+ String id = username.toLowerCase();
+ if (keys.containsKey(id)) {
+ return keys.get(id);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean addKey(String username, SshKey key) {
+ String id = username.toLowerCase();
+ if (!keys.containsKey(id)) {
+ keys.put(id, new ArrayList<SshKey>());
+ }
+ log.info("added {} key {}", username, key.getFingerprint());
+ return keys.get(id).add(key);
+ }
+
+ @Override
+ 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;
+ }
+ List<SshKey> 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/NonForwardingFilter.java b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java
new file mode 100644
index 00000000..0ed7926c
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java
@@ -0,0 +1,27 @@
+package com.gitblit.transport.ssh;
+
+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 canConnect(SshdSocketAddress address, Session session) {
+ return false;
+ }
+
+ @Override
+ public boolean canForwardAgent(Session session) {
+ return false;
+ }
+
+ @Override
+ public boolean canForwardX11(Session session) {
+ return false;
+ }
+
+ @Override
+ public boolean canListen(SshdSocketAddress address, Session session) {
+ return false;
+ }
+}
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..0761d842
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/NullKeyManager.java
@@ -0,0 +1,76 @@
+/*
+ * 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.List;
+
+/**
+ * Rejects all public key management requests.
+ *
+ * @author James Moger
+ *
+ */
+public class NullKeyManager extends IPublicKeyManager {
+
+ public NullKeyManager() {
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public NullKeyManager start() {
+ log.info(toString());
+ return this;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public NullKeyManager stop() {
+ return this;
+ }
+
+ @Override
+ protected boolean isStale(String username) {
+ return false;
+ }
+
+ @Override
+ protected List<SshKey> getKeysImpl(String username) {
+ return null;
+ }
+
+ @Override
+ public boolean addKey(String username, SshKey key) {
+ return false;
+ }
+
+ @Override
+ public boolean removeKey(String username, SshKey key) {
+ return false;
+ }
+
+ @Override
+ public boolean removeAllKeys(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
new file mode 100644
index 00000000..6956c120
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -0,0 +1,231 @@
+/*
+ * 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.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.InetSocketAddress;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.text.MessageFormat;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.sshd.SshServer;
+import org.apache.sshd.common.io.IoServiceFactoryFactory;
+import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
+import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.common.util.SecurityUtils;
+import org.bouncycastle.openssl.PEMWriter;
+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.IGitblit;
+import com.gitblit.transport.ssh.commands.SshCommandFactory;
+import com.gitblit.utils.IdGenerator;
+import com.gitblit.utils.JnaUtils;
+import com.gitblit.utils.StringUtils;
+import com.google.common.io.Files;
+
+/**
+ * Manager for the ssh transport. Roughly analogous to the
+ * {@link com.gitblit.transport.git.GitDaemon} class.
+ *
+ * @author Eric Myhre
+ *
+ */
+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
+ * what the git protocol itself defaults to if it sees and ssh url without a
+ * port.
+ */
+ public static final int DEFAULT_PORT = 22;
+
+ private final AtomicBoolean run;
+
+ private final IGitblit gitblit;
+ private final SshServer sshd;
+
+ /**
+ * Construct the Gitblit SSH daemon.
+ *
+ * @param gitblit
+ */
+ public SshDaemon(IGitblit gitblit, IdGenerator idGenerator) {
+ this.gitblit = gitblit;
+
+ IStoredSettings settings = gitblit.getSettings();
+
+ // Ensure that Bouncy Castle is our JCE provider
+ SecurityUtils.setRegisterBouncyCastle(true);
+
+ // Generate host RSA and DSA keypairs and create the host keypair provider
+ File rsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-rsa-hostkey.pem");
+ File dsaKeyStore = new File(gitblit.getBaseFolder(), "ssh-dsa-hostkey.pem");
+ generateKeyPair(rsaKeyStore, "RSA", 2048);
+ generateKeyPair(dsaKeyStore, "DSA", 0);
+ FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
+ hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() });
+
+ // Client public key authenticator
+ CachingPublicKeyAuthenticator keyAuthenticator =
+ new CachingPublicKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
+
+ // Configure the preferred SSHD backend
+ 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());
+
+ // Create the socket address for binding the SSH server
+ int port = settings.getInteger(Keys.git.sshPort, 0);
+ String bindInterface = settings.getString(Keys.git.sshBindInterface, "");
+ InetSocketAddress addr;
+ if (StringUtils.isEmpty(bindInterface)) {
+ addr = new InetSocketAddress(port);
+ } else {
+ addr = new InetSocketAddress(bindInterface, port);
+ }
+
+ // Create the SSH server
+ sshd = SshServer.setUpDefaultServer();
+ sshd.setPort(addr.getPort());
+ sshd.setHost(addr.getHostName());
+ sshd.setKeyPairProvider(hostKeyPairProvider);
+ 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, idGenerator));
+ sshd.setShellFactory(new WelcomeShell(settings));
+
+ // Set the server id. This can be queried with:
+ // ssh-keyscan -t rsa,dsa -p 29418 localhost
+ String version = String.format("%s (%s-%s)", Constants.getGitBlitVersion().replace(' ', '_'),
+ sshd.getVersion(), sshBackendStr);
+ sshd.getProperties().put(SshServer.SERVER_IDENTIFICATION, version);
+
+ run = new AtomicBoolean(false);
+ }
+
+ public String formatUrl(String gituser, String servername, String repository) {
+ 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, sshd.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);
+
+ String sshBackendStr = gitblit.getSettings().getString(Keys.git.sshBackend,
+ SshSessionBackend.NIO2.name());
+
+ log.info(MessageFormat.format(
+ "SSH Daemon ({0}) is listening on {1}:{2,number,0}",
+ sshBackendStr, sshd.getHost(), sshd.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 {
+ ((SshCommandFactory) sshd.getCommandFactory()).stop();
+ sshd.stop();
+ } catch (InterruptedException e) {
+ log.error("SSH Daemon stop interrupted", e);
+ }
+ }
+ }
+
+ private void generateKeyPair(File file, String algorithm, int keySize) {
+ if (file.exists()) {
+ return;
+ }
+ try {
+ KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator(algorithm);
+ if (keySize != 0) {
+ generator.initialize(keySize);
+ log.info("Generating {}-{} SSH host keypair...", algorithm, keySize);
+ } else {
+ log.info("Generating {} SSH host keypair...", algorithm);
+ }
+ KeyPair kp = generator.generateKeyPair();
+
+ // create an empty file and set the permissions
+ Files.touch(file);
+ try {
+ JnaUtils.setFilemode(file, JnaUtils.S_IRUSR | JnaUtils.S_IWUSR);
+ } catch (UnsupportedOperationException e) {
+ // Windows
+ }
+
+ FileOutputStream os = new FileOutputStream(file);
+ PEMWriter w = new PEMWriter(new OutputStreamWriter(os));
+ w.writeObject(kp);
+ w.flush();
+ w.close();
+ } catch (Exception e) {
+ log.warn(MessageFormat.format("Unable to generate {0} keypair", algorithm), e);
+ return;
+ }
+ }
+}
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..a5d4c3dd
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
@@ -0,0 +1,74 @@
+/*
+ * 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<SshDaemonClient> KEY = new AttributeKey<SshDaemonClient>();
+
+ private final SocketAddress remoteAddress;
+
+ private volatile UserModel user;
+ private volatile SshKey key;
+ 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;
+ }
+
+ public SshKey getKey() {
+ return key;
+ }
+
+ public void setKey(SshKey key) {
+ this.key = key;
+ }
+
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/SshKey.java b/src/main/java/com/gitblit/transport/ssh/SshKey.java
new file mode 100644
index 00000000..6a20d7dd
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/SshKey.java
@@ -0,0 +1,212 @@
+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;
+
+/**
+ * Class that encapsulates a public SSH key and it's metadata.
+ *
+ * @author James Moger
+ *
+ */
+public class SshKey implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private String rawData;
+
+ private PublicKey publicKey;
+
+ private String comment;
+
+ private String fingerprint;
+
+ 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() {
+ if (publicKey == null && rawData != null) {
+ // instantiate the public key from the raw key data
+ final String[] parts = rawData.split(" ", 3);
+ if (comment == null && parts.length == 3) {
+ comment = parts[2];
+ }
+ final byte[] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1]));
+ try {
+ publicKey = new Buffer(bin).getRawPublicKey();
+ } catch (SshException e) {
+ e.printStackTrace();
+ }
+ }
+ return publicKey;
+ }
+
+ public String getAlgorithm() {
+ return getPublicKey().getAlgorithm();
+ }
+
+ public String getComment() {
+ if (comment == null && rawData != null) {
+ // extract comment from the raw data
+ final String[] parts = rawData.split(" ", 3);
+ if (parts.length == 3) {
+ comment = parts[2];
+ }
+ }
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ if (rawData != null) {
+ rawData = null;
+ }
+ }
+
+ /**
+ * 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<AccessPermission> 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
+ Buffer buf = new Buffer();
+
+ // 1: identify the algorithm
+ buf.putRawPublicKey(publicKey);
+ String alg = buf.getString();
+
+ // 2: encode the key
+ buf.clear();
+ buf.putPublicKey(publicKey);
+ String b64 = Base64.encodeBase64String(buf.getBytes());
+
+ String c = getComment();
+ rawData = alg + " " + b64 + (StringUtils.isEmpty(c) ? "" : (" " + c));
+ }
+ return rawData;
+ }
+
+ public String getFingerprint() {
+ if (fingerprint == null) {
+ StringBuilder sb = new StringBuilder();
+ // append the key hash as colon-separated pairs
+ String hash;
+ if (rawData != null) {
+ final String[] parts = rawData.split(" ", 3);
+ final byte [] bin = Base64.decodeBase64(Constants.encodeASCII(parts[1]));
+ hash = StringUtils.getMD5(bin);
+ } else {
+ // 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(':');
+ }
+ sb.setLength(sb.length() - 1);
+ fingerprint = sb.toString();
+ }
+ return fingerprint;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof PublicKey) {
+ return getPublicKey().equals(o);
+ } else if (o instanceof SshKey) {
+ return getPublicKey().equals(((SshKey) o).getPublicKey());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getPublicKey().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ if (toString == null) {
+ StringBuilder sb = new StringBuilder();
+ // TODO append the keysize
+ int keySize = 0;
+ if (keySize > 0) {
+ sb.append(keySize).append(' ');
+ }
+ // append fingerprint
+ sb.append(' ');
+ sb.append(getFingerprint());
+ // append the comment
+ String c = getComment();
+ if (!StringUtils.isEmpty(c)) {
+ sb.append(' ');
+ sb.append(c);
+ }
+ // append algorithm
+ String alg = getAlgorithm();
+ if (!StringUtils.isEmpty(alg)) {
+ sb.append(" (").append(alg).append(")");
+ }
+ toString = sb.toString();
+ }
+ return toString;
+ }
+}
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<CloseFuture> 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..0c018f02
--- /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("creating ssh session from {}", io.getRemoteAddress());
+
+ 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<CloseFuture>() {
+ @Override
+ public void operationComplete(CloseFuture future) {
+ log.info("closed ssh session from {}", io.getRemoteAddress());
+ }
+ });
+ 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/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/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
new file mode 100644
index 00000000..acd3c15f
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
@@ -0,0 +1,212 @@
+/*
+ * 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.transport.ssh.commands.DispatchCommand;
+import com.gitblit.transport.ssh.commands.SshCommandFactory;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Class that displays a welcome message for any shell requests.
+ *
+ */
+public class WelcomeShell implements Factory<Command> {
+
+ 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 ServerSession session;
+
+ 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.session = session;
+ }
+
+ @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() {
+ this.session = null;
+ }
+
+ String getMessage() {
+ SshDaemonClient client = session.getAttribute(SshDaemonClient.KEY);
+ UserModel user = client.getUser();
+ String hostname = getHostname();
+ int port = settings.getInteger(Keys.git.sshPort, 0);
+
+ final String b1 = StringUtils.rightPad("", 72, '═');
+ final String b2 = StringUtils.rightPad("", 72, '─');
+ final String nl = "\r\n";
+
+ StringBuilder msg = new StringBuilder();
+ msg.append(nl);
+ msg.append(b1);
+ msg.append(nl);
+ msg.append(" ");
+ msg.append(com.gitblit.Constants.getGitBlitVersion());
+ msg.append(nl);
+ msg.append(b1);
+ msg.append(nl);
+ msg.append(nl);
+ msg.append(" Hi ");
+ msg.append(user.getDisplayName());
+ msg.append(", you have successfully connected over SSH.");
+ msg.append(nl);
+ msg.append(" Interactive shells are not available.");
+ msg.append(nl);
+ msg.append(nl);
+ msg.append(" client: ");
+ msg.append(session.getClientVersion());
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(b2);
+ msg.append(nl);
+ msg.append(nl);
+ msg.append(" You may clone a repository with the following Git syntax:");
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(" git clone ");
+ msg.append(formatUrl(hostname, port, user.username));
+ msg.append(nl);
+ msg.append(nl);
+
+ msg.append(b2);
+ msg.append(nl);
+ msg.append(nl);
+
+ if (client.getKey() == null) {
+ // user has authenticated with a password
+ // display add public key instructions
+ msg.append(" You may upload an SSH public key with the following syntax:");
+ msg.append(nl);
+ msg.append(nl);
+
+ 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);
+
+ msg.append(b2);
+ msg.append(nl);
+ msg.append(nl);
+ }
+
+ // display the core commands
+ SshCommandFactory cmdFactory = (SshCommandFactory) session.getFactoryManager().getCommandFactory();
+ DispatchCommand root = cmdFactory.createRootDispatcher(client, "");
+ String usage = root.usage().replace("\n", nl);
+ msg.append(usage);
+
+ return msg.toString();
+ }
+
+ private String getHostname() {
+ 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();
+ }
+ return host;
+ }
+
+ private String formatUrl(String hostname, int port, String username) {
+ if (port == 22) {
+ // standard port
+ return MessageFormat.format("{0}@{1}/REPOSITORY.git", username, hostname);
+ } else {
+ // non-standard port
+ return MessageFormat.format("ssh://{0}@{1}:{2,number,0}/REPOSITORY.git",
+ username, hostname, port);
+ }
+ }
+ }
+}
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..d6aa929f
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/BaseCommand.java
@@ -0,0 +1,557 @@
+// 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.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;
+
+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.Keys;
+import com.gitblit.utils.IdGenerator;
+import com.gitblit.utils.StringUtils;
+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;
+
+public abstract class BaseCommand implements Command, SessionAware {
+
+ private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
+
+ private static final int PRIVATE_STATUS = 1 << 30;
+
+ public final static int STATUS_CANCEL = PRIVATE_STATUS | 1;
+
+ public final static int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
+
+ public final static int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
+
+ protected InputStream in;
+
+ protected OutputStream out;
+
+ protected OutputStream err;
+
+ protected ExitCallback exit;
+
+ protected ServerSession session;
+
+ /** Ssh command context */
+ private SshCommandContext ctx;
+
+ /** 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<Future<?>> 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() {
+ log.debug("destroying " + getClass().getName());
+ session = null;
+ ctx = null;
+ }
+
+ 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;
+
+ protected void provideStateTo(final BaseCommand cmd) {
+ cmd.setContext(ctx);
+ cmd.setInputStream(in);
+ cmd.setOutputStream(out);
+ cmd.setErrorStream(err);
+ cmd.setExitCallback(exit);
+ }
+
+ public void setContext(SshCommandContext ctx) {
+ this.ctx = ctx;
+ }
+
+ public SshCommandContext getContext() {
+ return 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 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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()) {
+ CommandMetaData meta = getClass().getAnnotation(CommandMetaData.class);
+ String title = meta.name().toUpperCase() + ": " + meta.description();
+ String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
+ StringWriter msg = new StringWriter();
+ msg.write('\n');
+ msg.write(b);
+ msg.write('\n');
+ msg.write(' ');
+ msg.write(title);
+ msg.write('\n');
+ msg.write(b);
+ msg.write("\n\n");
+ msg.write("USAGE\n");
+ msg.write("─────\n");
+ msg.write(' ');
+ msg.write(commandName);
+ msg.write('\n');
+ msg.write(" ");
+ clp.printSingleLineUsage(msg, null);
+ msg.write("\n\n");
+ String txt = getUsageText();
+ if (!StringUtils.isEmpty(txt)) {
+ msg.write(txt);
+ msg.write("\n\n");
+ }
+ msg.write("ARGUMENTS & OPTIONS\n");
+ msg.write("───────────────────\n");
+ clp.printUsage(msg, null);
+ msg.write('\n');
+ String examples = usage().trim();
+ if (!StringUtils.isEmpty(examples)) {
+ msg.write('\n');
+ msg.write("EXAMPLES\n");
+ msg.write("────────\n");
+ msg.write(examples);
+ msg.write('\n');
+ }
+
+ 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);
+ }
+
+ public String usage() {
+ Class<? extends BaseCommand> clazz = getClass();
+ if (clazz.isAnnotationPresent(UsageExamples.class)) {
+ return examples(clazz.getAnnotation(UsageExamples.class).examples());
+ } else if (clazz.isAnnotationPresent(UsageExample.class)) {
+ return examples(clazz.getAnnotation(UsageExample.class));
+ }
+ return "";
+ }
+
+ protected String getUsageText() {
+ return "";
+ }
+
+ protected String examples(UsageExample... examples) {
+ int sshPort = getContext().getGitblit().getSettings().getInteger(Keys.git.sshPort, 29418);
+ String username = getContext().getClient().getUsername();
+ String hostname = "localhost";
+ String ssh = String.format("ssh -l %s -p %d %s", username, sshPort, hostname);
+
+ StringBuilder sb = new StringBuilder();
+ for (UsageExample example : examples) {
+ sb.append(example.description()).append("\n\n");
+ String syntax = example.syntax();
+ syntax = syntax.replace("${ssh}", ssh);
+ syntax = syntax.replace("${username}", username);
+ syntax = syntax.replace("${cmd}", commandName);
+ sb.append(" ").append(syntax).append("\n\n");
+ }
+ return sb.toString();
+ }
+
+ protected void showHelp() throws UnloggedFailure {
+ argv = new String [] { "--help" };
+ parseCommandLine();
+ }
+
+ 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.
+ * <p>
+ * Typically this should be invoked within
+ * {@link Command#start(Environment)}, such as:
+ *
+ * <pre>
+ * startThread(new Runnable() {
+ * public void run() {
+ * runImp();
+ * }
+ * });
+ * </pre>
+ *
+ * @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.
+ * <p>
+ * Commands should invoke this at most once.
+ *
+ * @param rc exit code for the remote client.
+ */
+ protected void onExit(final int rc) {
+ exit.onExit(rc);
+ }
+
+ 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");
+ String user = ctx.getClient().getUsername();
+ if (user != null) {
+ m.append(" (user ");
+ m.append(user);
+ m.append(")");
+ }
+ m.append(" during ");
+ m.append(ctx.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.
+ * <p>
+ * Typically this should be invoked within
+ * {@link Command#start(Environment)}, such as:
+ *
+ * <pre>
+ * startThread(new CommandRunnable() {
+ * public void run() throws Exception {
+ * runImp();
+ * }
+ * });
+ * </pre>
+ * <p>
+ * 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/CommandMetaData.java b/src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java
new file mode 100644
index 00000000..b3a9dd2c
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/CommandMetaData.java
@@ -0,0 +1,34 @@
+//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 [] aliases() default {};
+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
new file mode 100644
index 00000000..f8239b55
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/DispatchCommand.java
@@ -0,0 +1,415 @@
+// 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.text.MessageFormat;
+import java.util.ArrayList;
+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;
+import org.kohsuke.args4j.Argument;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import ro.fortsoft.pf4j.ExtensionPoint;
+
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.cli.SubcommandHandler;
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+
+public abstract class DispatchCommand extends BaseCommand implements ExtensionPoint {
+
+ 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<String> args = new ArrayList<String>();
+
+ private final Set<Class<? extends BaseCommand>> commands;
+ private final Map<String, DispatchCommand> dispatchers;
+ private final Map<String, String> aliasToCommand;
+ private final Map<String, List<String>> commandToAliases;
+ private final List<BaseCommand> instantiated;
+ private Map<String, Class<? extends BaseCommand>> map;
+
+ protected DispatchCommand() {
+ commands = new HashSet<Class<? extends BaseCommand>>();
+ dispatchers = Maps.newHashMap();
+ aliasToCommand = Maps.newHashMap();
+ commandToAliases = Maps.newHashMap();
+ instantiated = new ArrayList<BaseCommand>();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ commands.clear();
+ aliasToCommand.clear();
+ commandToAliases.clear();
+ map = null;
+
+ for (BaseCommand command : instantiated) {
+ command.destroy();
+ }
+ instantiated.clear();
+
+ for (DispatchCommand dispatcher : dispatchers.values()) {
+ dispatcher.destroy();
+ }
+ dispatchers.clear();
+ }
+
+ /**
+ * Setup this dispatcher. Commands and nested dispatchers are normally
+ * registered within this method.
+ *
+ * @param user
+ */
+ protected abstract void setup(UserModel user);
+
+ /**
+ * Register a command or a dispatcher by it's class.
+ *
+ * @param user
+ * @param clazz
+ */
+ @SuppressWarnings("unchecked")
+ protected final void register(UserModel user, Class<? extends BaseCommand> clazz) {
+ if (DispatchCommand.class.isAssignableFrom(clazz)) {
+ registerDispatcher(user, (Class<? extends DispatchCommand>) clazz);
+ return;
+ }
+
+ registerCommand(user, clazz);
+ }
+
+ /**
+ * Register a command or a dispatcher instance.
+ *
+ * @param user
+ * @param cmd
+ */
+ protected final void register(UserModel user, BaseCommand cmd) {
+ if (cmd instanceof DispatchCommand) {
+ registerDispatcher(user, (DispatchCommand) cmd);
+ return;
+ }
+ registerCommand(user, cmd);
+ }
+
+ private void registerDispatcher(UserModel user, Class<? extends DispatchCommand> clazz) {
+ try {
+ DispatchCommand dispatcher = clazz.newInstance();
+ registerDispatcher(user, dispatcher);
+ } catch (Exception e) {
+ log.error("failed to instantiate {}", clazz.getName());
+ }
+ }
+
+ private void registerDispatcher(UserModel user, DispatchCommand dispatcher) {
+ Class<? extends DispatchCommand> dispatcherClass = dispatcher.getClass();
+ if (!dispatcherClass.isAnnotationPresent(CommandMetaData.class)) {
+ throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", dispatcher.getName(),
+ CommandMetaData.class.getName()));
+ }
+
+ CommandMetaData meta = dispatcherClass.getAnnotation(CommandMetaData.class);
+ if (meta.admin() && !user.canAdmin()) {
+ log.debug(MessageFormat.format("excluding admin dispatcher {0} for {1}",
+ meta.name(), user.username));
+ return;
+ }
+
+ try {
+ dispatcher.setup(user);
+ if (dispatcher.commands.isEmpty() && dispatcher.dispatchers.isEmpty()) {
+ log.debug(MessageFormat.format("excluding empty dispatcher {0} for {1}",
+ meta.name(), user.username));
+ return;
+ }
+
+ log.debug("registering {} dispatcher", meta.name());
+ dispatchers.put(meta.name(), dispatcher);
+ for (String alias : meta.aliases()) {
+ aliasToCommand.put(alias, meta.name());
+ if (!commandToAliases.containsKey(meta.name())) {
+ commandToAliases.put(meta.name(), new ArrayList<String>());
+ }
+ commandToAliases.get(meta.name()).add(alias);
+ }
+ } catch (Exception e) {
+ log.error("failed to register {} dispatcher", meta.name());
+ }
+ }
+
+ /**
+ * Registers a command as long as the user is permitted to execute it.
+ *
+ * @param user
+ * @param clazz
+ */
+ private void registerCommand(UserModel user, Class<? extends BaseCommand> clazz) {
+ if (!clazz.isAnnotationPresent(CommandMetaData.class)) {
+ throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", clazz.getName(),
+ CommandMetaData.class.getName()));
+ }
+ CommandMetaData meta = clazz.getAnnotation(CommandMetaData.class);
+ if (meta.admin() && !user.canAdmin()) {
+ log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
+ return;
+ }
+ commands.add(clazz);
+ }
+
+ /**
+ * Registers a command as long as the user is permitted to execute it.
+ *
+ * @param user
+ * @param cmd
+ */
+ private void registerCommand(UserModel user, BaseCommand cmd) {
+ if (!cmd.getClass().isAnnotationPresent(CommandMetaData.class)) {
+ throw new RuntimeException(MessageFormat.format("{0} must be annotated with {1}!", cmd.getName(),
+ CommandMetaData.class.getName()));
+ }
+ CommandMetaData meta = cmd.getClass().getAnnotation(CommandMetaData.class);
+ if (meta.admin() && !user.canAdmin()) {
+ log.debug(MessageFormat.format("excluding admin command {0} for {1}", meta.name(), user.username));
+ return;
+ }
+ commands.add(cmd.getClass());
+ instantiated.add(cmd);
+ }
+
+ private Map<String, Class<? extends BaseCommand>> getMap() {
+ if (map == null) {
+ map = Maps.newHashMapWithExpectedSize(commands.size());
+ for (Class<? extends BaseCommand> cmd : commands) {
+ CommandMetaData meta = cmd.getAnnotation(CommandMetaData.class);
+ if (map.containsKey(meta.name()) || aliasToCommand.containsKey(meta.name())) {
+ log.warn("{} already contains the \"{}\" command!", getName(), meta.name());
+ } else {
+ map.put(meta.name(), cmd);
+ }
+ for (String alias : meta.aliases()) {
+ if (map.containsKey(alias) || aliasToCommand.containsKey(alias)) {
+ log.warn("{} already contains the \"{}\" command!", getName(), alias);
+ } else {
+ aliasToCommand.put(alias, meta.name());
+ if (!commandToAliases.containsKey(meta.name())) {
+ commandToAliases.put(meta.name(), new ArrayList<String>());
+ }
+ commandToAliases.get(meta.name()).add(alias);
+ }
+ }
+ }
+
+ for (Map.Entry<String, DispatchCommand> entry : dispatchers.entrySet()) {
+ map.put(entry.getKey(), entry.getValue().getClass());
+ }
+ }
+ 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());
+ }
+
+ 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);
+ 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 BaseCommand getCommand() throws UnloggedFailure {
+ Map<String, Class<? extends BaseCommand>> map = getMap();
+ String name = commandName;
+ if (aliasToCommand.containsKey(commandName)) {
+ name = aliasToCommand.get(name);
+ }
+ if (dispatchers.containsKey(name)) {
+ return dispatchers.get(name);
+ }
+ final Class<? extends BaseCommand> c = map.get(name);
+ if (c == null) {
+ String msg = (getName().isEmpty() ? "Gitblit" : getName()) + ": " + commandName + ": not found";
+ throw new UnloggedFailure(1, msg);
+ }
+
+ for (BaseCommand cmd : instantiated) {
+ // use an already instantiated command
+ if (cmd.getClass().equals(c)) {
+ return cmd;
+ }
+ }
+
+ BaseCommand cmd = null;
+ try {
+ cmd = c.newInstance();
+ instantiated.add(cmd);
+ } catch (Exception e) {
+ throw new UnloggedFailure(1, MessageFormat.format("Failed to instantiate {0} command", commandName));
+ }
+ return cmd;
+ }
+
+ private boolean hasVisibleCommands() {
+ boolean visible = false;
+ for (Class<? extends BaseCommand> cmd : commands) {
+ visible |= !cmd.getAnnotation(CommandMetaData.class).hidden();
+ if (visible) {
+ return true;
+ }
+ }
+ for (DispatchCommand cmd : dispatchers.values()) {
+ visible |= cmd.hasVisibleCommands();
+ if (visible) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String getDescription() {
+ return getClass().getAnnotation(CommandMetaData.class).description();
+ }
+
+ @Override
+ public String usage() {
+ Set<String> cmds = new TreeSet<String>();
+ Set<String> dcs = new TreeSet<String>();
+ Map<String, String> displayNames = Maps.newHashMap();
+ int maxLength = -1;
+ Map<String, Class<? extends BaseCommand>> m = getMap();
+ for (String name : m.keySet()) {
+ Class<? extends BaseCommand> c = m.get(name);
+ CommandMetaData meta = c.getAnnotation(CommandMetaData.class);
+ if (meta.hidden()) {
+ continue;
+ }
+
+ String displayName = name + (meta.admin() ? "*" : "");
+ if (commandToAliases.containsKey(meta.name())) {
+ displayName = name + (meta.admin() ? "*" : "")+ " (" + Joiner.on(',').join(commandToAliases.get(meta.name())) + ")";
+ }
+ displayNames.put(name, displayName);
+
+ maxLength = Math.max(maxLength, displayName.length());
+ if (DispatchCommand.class.isAssignableFrom(c)) {
+ DispatchCommand d = dispatchers.get(name);
+ if (d.hasVisibleCommands()) {
+ dcs.add(name);
+ }
+ } else {
+ cmds.add(name);
+ }
+ }
+ String format = "%-" + maxLength + "s %s";
+
+ final StringBuilder usage = new StringBuilder();
+ if (!StringUtils.isEmpty(getName())) {
+ String title = getName().toUpperCase() + ": " + getDescription();
+ String b = com.gitblit.utils.StringUtils.leftPad("", title.length() + 2, '═');
+ usage.append('\n');
+ usage.append(b).append('\n');
+ usage.append(' ').append(title).append('\n');
+ usage.append(b).append('\n');
+ usage.append('\n');
+ }
+
+ if (!cmds.isEmpty()) {
+ usage.append("Available commands");
+ if (!getName().isEmpty()) {
+ usage.append(" of ");
+ usage.append(getName());
+ }
+ usage.append(" are:\n");
+ usage.append("\n");
+ for (String name : cmds) {
+ final Class<? extends Command> c = m.get(name);
+ String displayName = displayNames.get(name);
+ CommandMetaData meta = c.getAnnotation(CommandMetaData.class);
+ usage.append(" ");
+ usage.append(String.format(format, displayName, Strings.nullToEmpty(meta.description())));
+ usage.append("\n");
+ }
+ usage.append("\n");
+ }
+
+ if (!dcs.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 : dcs) {
+ final Class<? extends BaseCommand> c = m.get(name);
+ String displayName = displayNames.get(name);
+ CommandMetaData meta = c.getAnnotation(CommandMetaData.class);
+ usage.append(" ");
+ usage.append(String.format(format, displayName, Strings.nullToEmpty(meta.description())));
+ usage.append("\n");
+ }
+ usage.append("\n");
+ }
+
+ usage.append("See '");
+ if (!StringUtils.isEmpty(getName())) {
+ 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/ListCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ListCommand.java
new file mode 100644
index 00000000..8d4ddc3c
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/ListCommand.java
@@ -0,0 +1,92 @@
+/*
+ * 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.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import org.kohsuke.args4j.Option;
+
+import com.gitblit.utils.JsonUtils;
+
+/**
+ * Parent class of a list command.
+ *
+ * @author James Moger
+ *
+ * @param <T>
+ */
+public abstract class ListCommand<T> extends SshCommand {
+
+ @Option(name = "--verbose", aliases = { "-v" }, usage = "verbose")
+ protected boolean verbose;
+
+ @Option(name = "--tabbed", usage = "generate tabbed-text output")
+ protected boolean tabbed;
+
+ @Option(name = "--json", usage = "generate JSON output")
+ protected boolean json;
+
+ private DateFormat df;
+
+ protected abstract List<T> getItems() throws UnloggedFailure;
+
+ protected void validateOutputFormat() throws UnloggedFailure {
+ if (tabbed && json) {
+ throw new UnloggedFailure(1, "Please specify --tabbed OR --json, not both!");
+ }
+ }
+
+ @Override
+ public void run() throws UnloggedFailure {
+ validateOutputFormat();
+
+ List<T> list = getItems();
+ if (tabbed) {
+ asTabbed(list);
+ } else if (json) {
+ asJSON(list);
+ } else {
+ asTable(list);
+ }
+ }
+
+ protected abstract void asTable(List<T> list);
+
+ protected abstract void asTabbed(List<T> list);
+
+ protected void outTabbed(Object... values) {
+ StringBuilder pattern = new StringBuilder();
+ for (int i = 0; i < values.length; i++) {
+ pattern.append("%s\t");
+ }
+ pattern.setLength(pattern.length() - 1);
+ stdout.println(String.format(pattern.toString(), values));
+ }
+
+ protected void asJSON(List<T> list) {
+ stdout.println(JsonUtils.toJsonString(list));
+ }
+
+ protected String formatDate(Date date) {
+ if (df == null) {
+ df = new SimpleDateFormat("yyyy-MM-dd");
+ }
+ return df.format(date);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/ListFilterCommand.java b/src/main/java/com/gitblit/transport/ssh/commands/ListFilterCommand.java
new file mode 100644
index 00000000..4cc0983e
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/ListFilterCommand.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.commands;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.kohsuke.args4j.Argument;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * List command that accepts a filter parameter.
+ *
+ * @author James Moger
+ *
+ * @param <T>
+ */
+public abstract class ListFilterCommand<T> extends ListCommand<T> {
+
+ @Argument(index = 0, metaVar = "FILTER", usage = "filter expression")
+ private String filter;
+
+ protected abstract boolean matches(String filter, T t);
+
+ @Override
+ public void run() throws UnloggedFailure {
+ validateOutputFormat();
+
+ List<T> list = getItems();
+ List<T> filtered;
+ if (StringUtils.isEmpty(filter)) {
+ // no filter
+ filtered = list;
+ } else {
+ // filter the list
+ filtered = new ArrayList<T>();
+ for (T t : list) {
+ if (matches(filter, t)) {
+ filtered.add(t);
+ }
+ }
+ }
+
+ if (tabbed) {
+ asTabbed(filtered);
+ } else if (json) {
+ asJSON(filtered);
+ } else {
+ asTable(filtered);
+ }
+ }
+} \ No newline at end of file
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..19cefe02
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java
@@ -0,0 +1,556 @@
+/*
+ * 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.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.
+ *
+ * @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, EnablePlugin.class);
+ register(user, DisablePlugin.class);
+ register(user, ShowPlugin.class);
+ register(user, RefreshPlugins.class);
+ register(user, AvailablePlugins.class);
+ register(user, InstallPlugin.class);
+ register(user, UninstallPlugin.class);
+ }
+
+ @CommandMetaData(name = "list", aliases = { "ls" }, description = "List plugins")
+ public static class ListPlugins extends ListCommand<PluginWrapper> {
+
+ @Override
+ protected List<PluginWrapper> getItems() throws UnloggedFailure {
+ IGitblit gitblit = getContext().getGitblit();
+ List<PluginWrapper> list = gitblit.getPlugins();
+ return list;
+ }
+
+ @Override
+ protected void asTable(List<PluginWrapper> list) {
+ String[] headers;
+ if (verbose) {
+ String [] h = { "#", "Id", "Version", "State", "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.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<PluginWrapper> list) {
+ for (PluginWrapper pw : list) {
+ PluginDescriptor d = pw.getDescriptor();
+ if (verbose) {
+ outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath(), d.getProvider());
+ } else {
+ outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath());
+ }
+ }
+ }
+ }
+
+ 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<PluginWrapper> 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 PluginCommand {
+
+ @Argument(index = 0, required = true, metaVar = "ALL|<id>", usage = "the plugin to start")
+ protected String id;
+
+ @Override
+ public void run() throws Failure {
+ IGitblit gitblit = getContext().getGitblit();
+ if (id.equalsIgnoreCase("ALL")) {
+ gitblit.startPlugins();
+ stdout.println("All plugins started");
+ } else {
+ PluginWrapper pluginWrapper = getPlugin(id);
+ if (pluginWrapper == null) {
+ throw new UnloggedFailure(String.format("Plugin %s is not installed!", id));
+ }
+
+ 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 PluginCommand {
+
+ @Argument(index = 0, required = true, metaVar = "ALL|<id>", usage = "the plugin to stop")
+ protected String id;
+
+ @Override
+ public void run() throws Failure {
+ IGitblit gitblit = getContext().getGitblit();
+ if (id.equalsIgnoreCase("ALL")) {
+ gitblit.stopPlugins();
+ stdout.println("All plugins stopped");
+ } else {
+ PluginWrapper pluginWrapper = getPlugin(id);
+ if (pluginWrapper == null) {
+ throw new UnloggedFailure(String.format("Plugin %s is not installed!", id));
+ }
+
+ 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 = "<id>", 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 {
+
+ @Argument(index = 0, required = true, metaVar = "<id>", 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!");
+ }
+
+ 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 PluginCommand {
+
+ @Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin to show")
+ protected String id;
+
+ @Override
+ public void run() throws Failure {
+ IGitblit gitblit = getContext().getGitblit();
+ 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());
+ }
+
+ StringBuilder sb = new StringBuilder();
+ 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();
+ }
+
+ protected String buildReleaseTable(PluginRegistration reg) {
+ List<PluginRelease> 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<Class<?>> exts = gitblit.getExtensionClasses(pw.getPluginId());
+ String extensions;
+ if (exts.isEmpty()) {
+ extensions = FlipTable.EMPTY;
+ } else {
+ StringBuilder description = new StringBuilder();
+ for (int i = 0; i < exts.size(); i++) {
+ 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 = description.toString();
+ }
+
+ // DEPENDENCIES
+ sb.setLength(0);
+ List<PluginDependency> deps = pw.getDescriptor().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[i] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
+ }
+ dependencies = FlipTable.of(headers, data, Borders.COLS);
+ }
+
+ // 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));
+ }
+
+ /* Find the ExtensionPoint */
+ protected Class<?> getExtensionPoint(Class<?> clazz) {
+ Class<?> superClass = clazz.getSuperclass();
+ if (ExtensionPoint.class.isAssignableFrom(superClass)) {
+ return superClass;
+ }
+ return getExtensionPoint(superClass);
+ }
+ }
+
+ @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(!disableChecksum);
+ }
+ }
+
+ @CommandMetaData(name = "available", description = "List the available plugins")
+ public static class AvailablePlugins extends ListFilterCommand<PluginRegistration> {
+
+ @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;
+
+ @Option(name = "--noverify", usage = "Disable checksum verification")
+ private boolean disableChecksum;
+
+ @Override
+ protected List<PluginRegistration> getItems() throws UnloggedFailure {
+ IGitblit gitblit = getContext().getGitblit();
+ if (refresh) {
+ gitblit.refreshRegistry(!disableChecksum);
+ }
+
+ List<PluginRegistration> list;
+ if (updates) {
+ list = gitblit.getRegisteredPlugins(InstallState.CAN_UPDATE);
+ } else {
+ 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<PluginRegistration> list) {
+ String[] headers;
+ if (verbose) {
+ String [] h = { "Id", "Name", "Description", "Installed", "Current", "Requires", "State", "Registry" };
+ headers = h;
+ } else {
+ 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.id, p.name, p.description, p.installedRelease, curr.version, curr.requires, p.getInstallState(), p.registry};
+ } else {
+ data[i] = new Object[] {p.id, p.name, p.installedRelease, curr.version, curr.requires, p.getInstallState()};
+ }
+ }
+
+ stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
+ }
+
+ @Override
+ protected void asTabbed(List<PluginRegistration> list) {
+ for (PluginRegistration p : list) {
+ PluginRelease curr = p.getCurrentRelease();
+ if (verbose) {
+ outTabbed(p.id, p.name, p.description, p.installedRelease, curr.version, curr.requires, p.getInstallState(), p.provider, p.registry);
+ } else {
+ 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 = "<URL>|<ID>|<NAME>", 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 = "<id>", 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
new file mode 100644
index 00000000..bebb4ac9
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
@@ -0,0 +1,65 @@
+/*
+ * 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.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;
+import com.gitblit.transport.ssh.git.GitDispatcher;
+import com.gitblit.transport.ssh.keys.KeysDispatcher;
+
+/**
+ * The root dispatcher is the dispatch command that handles registering all
+ * other commands.
+ *
+ */
+@CommandMetaData(name = "")
+class RootDispatcher extends DispatchCommand {
+
+ private Logger log = LoggerFactory.getLogger(getClass());
+
+ public RootDispatcher(IGitblit gitblit, SshDaemonClient client, String cmdLine) {
+ super();
+ setContext(new SshCommandContext(gitblit, client, cmdLine));
+
+ UserModel user = client.getUser();
+ register(user, VersionCommand.class);
+ register(user, GitDispatcher.class);
+ register(user, KeysDispatcher.class);
+ register(user, PluginDispatcher.class);
+
+ List<DispatchCommand> exts = gitblit.getExtensions(DispatchCommand.class);
+ for (DispatchCommand ext : exts) {
+ Class<? extends DispatchCommand> extClass = ext.getClass();
+ PluginWrapper wrapper = gitblit.whichPlugin(extClass);
+ String plugin = wrapper.getDescriptor().getPluginId();
+ CommandMetaData meta = extClass.getAnnotation(CommandMetaData.class);
+ log.debug("Dispatcher {} is loaded from plugin {}", meta.name(), plugin);
+ register(user, ext);
+ }
+ }
+
+ @Override
+ protected final void setup(UserModel user) {
+ }
+} \ No newline at end of file
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..7008b5eb
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommand.java
@@ -0,0 +1,87 @@
+// 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 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());
+ 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 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;
+}
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..55090fcf
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/SshCommandFactory.java
@@ -0,0 +1,277 @@
+/*
+ * 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.ExecutorService;
+import java.util.concurrent.Executors;
+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.Keys;
+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;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+/**
+ *
+ * @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;
+ private final ExecutorService destroyExecutor;
+
+ public SshCommandFactory(IGitblit gitblit, IdGenerator idGenerator) {
+ this.gitblit = gitblit;
+
+ int threads = gitblit.getSettings().getInteger(Keys.git.sshCommandStartThreads, 2);
+ WorkQueue workQueue = new WorkQueue(idGenerator);
+ startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+ destroyExecutor = Executors.newSingleThreadExecutor(
+ new ThreadFactoryBuilder()
+ .setNameFormat("SshCommandDestroy-%s")
+ .setDaemon(true)
+ .build());
+ }
+
+ public void stop() {
+ destroyExecutor.shutdownNow();
+ }
+
+ 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<Future<?>> 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) {
+ 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)) {
+ 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) {
+ try {
+ cmd.destroy();
+ } finally {
+ cmd = null;
+ }
+ }
+ }
+ }
+ }
+
+ /** Split a command line into a string array. */
+ static public String[] split(String commandLine) {
+ final List<String> list = new ArrayList<String>();
+ 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/commands/UsageExample.java b/src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java
new file mode 100644
index 00000000..428dfde8
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/UsageExample.java
@@ -0,0 +1,32 @@
+/*
+ * 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 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 how to use it.
+*/
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface UsageExample {
+String syntax();
+String description() default "";
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java b/src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java
new file mode 100644
index 00000000..0193a98a
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/commands/UsageExamples.java
@@ -0,0 +1,31 @@
+/*
+ * 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 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 how to use it.
+*/
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface UsageExamples {
+UsageExample [] examples() default {};
+}
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/git/BaseGitCommand.java b/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java
new file mode 100644
index 00000000..fcb06568
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/git/BaseGitCommand.java
@@ -0,0 +1,114 @@
+/*
+ * 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<SshDaemonClient> repositoryResolver;
+ protected ReceivePackFactory<SshDaemonClient> receivePackFactory;
+ protected UploadPackFactory<SshDaemonClient> uploadPackFactory;
+
+ protected Repository repo;
+
+ @Override
+ public void destroy() {
+ super.destroy();
+
+ repositoryResolver = null;
+ receivePackFactory = null;
+ uploadPackFactory = null;
+ repo = null;
+ }
+
+ @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(getContext().getClient(), repository);
+ } catch (Exception e) {
+ throw new Failure(1, "fatal: '" + repository + "': not a git archive", e);
+ }
+ }
+
+ public void setRepositoryResolver(RepositoryResolver<SshDaemonClient> repositoryResolver) {
+ this.repositoryResolver = repositoryResolver;
+ }
+
+ public void setReceivePackFactory(GitblitReceivePackFactory<SshDaemonClient> receivePackFactory) {
+ this.receivePackFactory = receivePackFactory;
+ }
+
+ public void setUploadPackFactory(GitblitUploadPackFactory<SshDaemonClient> uploadPackFactory) {
+ this.uploadPackFactory = uploadPackFactory;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java b/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java
new file mode 100644
index 00000000..5383786a
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/git/GarbageCollectionCommand.java
@@ -0,0 +1,63 @@
+/*
+ * 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 java.util.Properties;
+
+import org.eclipse.jgit.api.GarbageCollectCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.transport.ssh.commands.CommandMetaData;
+import com.gitblit.transport.ssh.commands.UsageExample;
+
+@CommandMetaData(name = "gc", description = "Cleanup unnecessary files and optimize the local repository", admin = true)
+@UsageExample(syntax = "${cmd} test/myrepository.git", description = "Garbage collect \"test/myrepository.git\"")
+public class GarbageCollectionCommand extends BaseGitCommand {
+
+ private static final Logger log = LoggerFactory.getLogger(GarbageCollectionCommand.class);
+
+ @Override
+ protected void runImpl() throws IOException, Failure {
+ try {
+ GarbageCollectCommand gc = Git.wrap(repo).gc();
+ logGcInfo("before:", gc.getStatistics());
+ gc.setProgressMonitor(NullProgressMonitor.INSTANCE);
+ Properties statistics = gc.call();
+ logGcInfo("after: ", statistics);
+ } catch (Exception e) {
+ throw new Failure(1, "fatal: Cannot run gc: ", e);
+ }
+ }
+
+ private static void logGcInfo(String msg,
+ Properties statistics) {
+ StringBuilder b = new StringBuilder();
+ b.append(msg);
+ if (statistics != null) {
+ b.append(" ");
+ String s = statistics.toString();
+ if (s.startsWith("{") && s.endsWith("}")) {
+ s = s.substring(1, s.length() - 1);
+ }
+ b.append(s);
+ }
+ log.info(b.toString());
+ }
+}
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..64f9c8d0
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/git/GitDispatcher.java
@@ -0,0 +1,71 @@
+/*
+ * 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="Git repository commands")
+public class GitDispatcher extends DispatchCommand {
+
+ protected RepositoryResolver<SshDaemonClient> repositoryResolver;
+ protected GitblitUploadPackFactory<SshDaemonClient> uploadPackFactory;
+ protected GitblitReceivePackFactory<SshDaemonClient> receivePackFactory;
+
+ @Override
+ public void setContext(SshCommandContext context) {
+ super.setContext(context);
+
+ IGitblit gitblit = context.getGitblit();
+ repositoryResolver = new RepositoryResolver<SshDaemonClient>(gitblit);
+ uploadPackFactory = new GitblitUploadPackFactory<SshDaemonClient>(gitblit);
+ receivePackFactory = new GitblitReceivePackFactory<SshDaemonClient>(gitblit);
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+
+ repositoryResolver = null;
+ receivePackFactory = null;
+ uploadPackFactory = null;
+ }
+
+ @Override
+ protected void setup(UserModel user) {
+ register(user, Upload.class);
+ register(user, Receive.class);
+ register(user, GarbageCollectionCommand.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..f0d86f0d
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/git/Receive.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.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);
+ } 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..11a33cef
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/git/Upload.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.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)
+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) {
+ 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/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<String> readKeys(List<String> 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..3f581462
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
@@ -0,0 +1,268 @@
+/*
+ * 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.Constants.AccessPermission;
+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.gitblit.utils.StringUtils;
+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 = "<STDIN>", usage = "the key to add")
+ private List<String> addKeys = new ArrayList<String>();
+
+ @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, Failure {
+ String username = getContext().getClient().getUsername();
+ List<String> 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 Failure(1, e.getMessage());
+ }
+ }
+ }
+ 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 = "<INDEX>|ALL", usage = "the key to remove", required = true)
+ private List<String> keyParameters = new ArrayList<String>();
+
+ @Override
+ 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<SshKey> registeredKeys = new ArrayList<SshKey>(getKeyManager().getKeys(username));
+ if (registeredKeys.isEmpty()) {
+ throw new UnloggedFailure(1, "There are no registered keys!");
+ }
+
+ if (keyParameters.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 keyParameter : keyParameters) {
+ try {
+ // remove a key by it's index (1-based indexing)
+ 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 Failure(1, String.format("Invalid index specified. There are %d registered keys.", registeredKeys.size()));
+ }
+ SshKey sshKey = registeredKeys.get(index - 1);
+ if (getKeyManager().removeKey(username, sshKey)) {
+ stdout.println(String.format("Removed %s", sshKey.getFingerprint()));
+ } else {
+ 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));
+ }
+ }
+ }
+ }
+ }
+
+ @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<SshKey> keys = keyManager.getKeys(username);
+
+ if (showRaw) {
+ asRaw(keys);
+ } else {
+ asTable(keys);
+ }
+ }
+
+ /* output in the same format as authorized_keys */
+ protected void asRaw(List<SshKey> keys) {
+ if (keys == null) {
+ return;
+ }
+ for (SshKey key : keys) {
+ stdout.println(key.getRawData());
+ }
+ }
+
+ protected void asTable(List<SshKey> keys) {
+ 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.getPermission(), 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<SshKey> 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", "Permission", "Type" };
+ Object[][] data = new Object[1][];
+ data[0] = new Object[] { index, key.getFingerprint(), key.getComment(), key.getPermission(), 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<String> values = new ArrayList<String>();
+
+ @Override
+ public void run() throws Failure {
+ final String username = getContext().getClient().getUsername();
+ IPublicKeyManager keyManager = getContext().getGitblit().getPublicKeyManager();
+ List<SshKey> 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 Failure(1, String.format("Failed to update the comment for key #%d!", index));
+ }
+ }
+
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/FlipTable.java b/src/main/java/com/gitblit/utils/FlipTable.java
new file mode 100644
index 00000000..0197517d
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/FlipTable.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2014 Jake Wharton
+ * 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.utils;
+
+
+/**
+ * This is a forked version of FlipTables which supports controlling the
+ * displayed borders and gracefully handles null cell values.
+ *
+ * FULL = all borders
+ * BODY_COLS = header + perimeter + column separators
+ * COLS = header + column separators
+ * BODY = header + perimeter
+ * HEADER = header only
+ *
+ * <pre>
+ * ╔═════════════╤════════════════════════════╤══════════════╗
+ * ║ Name │ Function │ Author ║
+ * ╠═════════════╪════════════════════════════╪══════════════╣
+ * ║ Flip Tables │ Pretty-print a text table. │ Jake Wharton ║
+ * ╚═════════════╧════════════════════════════╧══════════════╝
+ * </pre>
+ */
+public final class FlipTable {
+ public static final String EMPTY = "(empty)";
+
+ public static enum Borders {
+ FULL(15), BODY_HCOLS(13), HCOLS(12), BODY(9), HEADER(8), COLS(4);
+
+ final int bitmask;
+
+ private Borders(int bitmask) {
+ this.bitmask = bitmask;
+ }
+
+ boolean header() {
+ return isset(0x8);
+ }
+
+ boolean body() {
+ return isset(0x1);
+ }
+
+ boolean rows() {
+ return isset(0x2);
+ }
+
+ boolean columns() {
+ return isset(0x4);
+ }
+
+ boolean isset(int v) {
+ return (bitmask & v) == v;
+ }
+ }
+
+ /** Create a new table with the specified headers and row data. */
+ public static String of(String[] headers, Object[][] data) {
+ return of(headers, data, Borders.FULL);
+ }
+
+ /** Create a new table with the specified headers and row data. */
+ public static String of(String[] headers, Object[][] data, Borders borders) {
+ if (headers == null)
+ throw new NullPointerException("headers == null");
+ if (headers.length == 0)
+ throw new IllegalArgumentException("Headers must not be empty.");
+ if (data == null)
+ throw new NullPointerException("data == null");
+ return new FlipTable(headers, data, borders).toString();
+ }
+
+ private final String[] headers;
+ private final Object[][] data;
+ private final Borders borders;
+ private final int columns;
+ private final int[] columnWidths;
+ private final int emptyWidth;
+
+ private FlipTable(String[] headers, Object[][] data, Borders borders) {
+ this.headers = headers;
+ this.data = data;
+ this.borders = borders;
+
+ columns = headers.length;
+ columnWidths = new int[columns];
+ for (int row = -1; row < data.length; row++) {
+ Object[] rowData = (row == -1) ? headers : data[row];
+ if (rowData.length != columns) {
+ throw new IllegalArgumentException(String.format("Row %s's %s columns != %s columns", row + 1,
+ rowData.length, columns));
+ }
+ for (int column = 0; column < columns; column++) {
+ Object cell = rowData[column];
+ if (cell == null) {
+ continue;
+ }
+ for (String rowDataLine : cell.toString().split("\\n")) {
+ columnWidths[column] = Math.max(columnWidths[column], rowDataLine.length());
+ }
+ }
+ }
+
+ // Account for column dividers and their spacing.
+ int emptyWidth = 3 * (columns - 1);
+ for (int columnWidth : columnWidths) {
+ emptyWidth += columnWidth;
+ }
+ this.emptyWidth = emptyWidth;
+
+ if (emptyWidth < EMPTY.length()) {
+ // Make sure we're wide enough for the empty text.
+ columnWidths[columns - 1] += EMPTY.length() - emptyWidth;
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ if (borders.header()) {
+ printDivider(builder, "╔═╤═╗");
+ }
+ printData(builder, headers, true);
+ if (data.length == 0) {
+ if (borders.body()) {
+ printDivider(builder, "╠═╧═╣");
+ builder.append('║').append(pad(emptyWidth, EMPTY)).append("║\n");
+ printDivider(builder, "╚═══╝");
+ } else if (borders.header()) {
+ printDivider(builder, "╚═╧═╝");
+ builder.append(' ').append(pad(emptyWidth, EMPTY)).append(" \n");
+ }
+ } else {
+ for (int row = 0; row < data.length; row++) {
+ if (row == 0 && borders.header()) {
+ if (borders.body()) {
+ if (borders.columns()) {
+ printDivider(builder, "╠═╪═╣");
+ } else {
+ printDivider(builder, "╠═╧═╣");
+ }
+ } else {
+ if (borders.columns()) {
+ printDivider(builder, "╚═╪═╝");
+ } else {
+ printDivider(builder, "╚═╧═╝");
+ }
+ }
+ } else if (row == 0 && !borders.header()) {
+ if (borders.columns()) {
+ printDivider(builder, " ─┼─ ");
+ } else {
+ printDivider(builder, " ─┼─ ");
+ }
+ } else if (borders.rows()) {
+ if (borders.columns()) {
+ printDivider(builder, "╟─┼─╢");
+ } else {
+ printDivider(builder, "╟─┼─╢");
+ }
+ }
+ printData(builder, data[row], false);
+ }
+ if (borders.body()) {
+ if (borders.columns()) {
+ printDivider(builder, "╚═╧═╝");
+ } else {
+ printDivider(builder, "╚═══╝");
+ }
+ }
+ }
+ return builder.toString();
+ }
+
+ private void printDivider(StringBuilder out, String format) {
+ for (int column = 0; column < columns; column++) {
+ out.append(column == 0 ? format.charAt(0) : format.charAt(2));
+ out.append(pad(columnWidths[column], "").replace(' ', format.charAt(1)));
+ }
+ out.append(format.charAt(4)).append('\n');
+ }
+
+ private void printData(StringBuilder out, Object[] data, boolean isHeader) {
+ for (int line = 0, lines = 1; line < lines; line++) {
+ for (int column = 0; column < columns; column++) {
+ if (column == 0) {
+ if ((isHeader && borders.header()) || borders.body()) {
+ out.append('║');
+ } else {
+ out.append(' ');
+ }
+ } else if (isHeader || borders.columns()) {
+ out.append('│');
+ } else {
+ out.append(' ');
+ }
+ Object cell = data[column];
+ if (cell == null) {
+ cell = "";
+ }
+ String[] cellLines = cell.toString().split("\\n");
+ lines = Math.max(lines, cellLines.length);
+ String cellLine = line < cellLines.length ? cellLines[line] : "";
+ out.append(pad(columnWidths[column], cellLine));
+ }
+ if ((isHeader && borders.header()) || borders.body()) {
+ out.append("║\n");
+ } else {
+ out.append('\n');
+ }
+ }
+ }
+
+ private static String pad(int width, String data) {
+ return String.format(" %1$-" + width + "s ", data);
+ }
+}
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/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) {
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> {
+ 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..ba49a4c5
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/WorkQueue.java
@@ -0,0 +1,346 @@
+// 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.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 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 {
+ 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<Executor> queues;
+
+ public WorkQueue(final IdGenerator idGenerator) {
+ this.idGenerator = idGenerator;
+ this.queues = new CopyOnWriteArrayList<Executor>();
+ }
+
+ /** 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<Task<?>> getTasks() {
+ final List<Task<?>> r = new ArrayList<Task<?>>();
+ for (final Executor e : queues) {
+ e.addAllTo(r);
+ }
+ return r;
+ }
+
+ public <T> List<T> getTaskInfos(TaskInfoFactory<T> factory) {
+ List<T> 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<Integer, Task<?>> 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<Integer, Task<?>>( //
+ corePoolSize << 1, // table size
+ 0.75f, // load factor
+ corePoolSize + 4 // concurrency level
+ );
+ }
+
+ public void unregisterWorkQueue() {
+ queues.remove(this);
+ }
+
+ @Override
+ protected <V> RunnableScheduledFuture<V> decorateTask(
+ final Runnable runnable, RunnableScheduledFuture<V> r) {
+ r = super.decorateTask(runnable, r);
+ for (;;) {
+ final int id = idGenerator.next();
+
+ Task<V> task;
+ task = new Task<V>(runnable, r, this, id);
+
+ if (all.putIfAbsent(task.getTaskId(), task) == null) {
+ return task;
+ }
+ }
+ }
+
+ @Override
+ protected <V> RunnableScheduledFuture<V> decorateTask(
+ final Callable<V> callable, final RunnableScheduledFuture<V> 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<Task<?>> list) {
+ list.addAll(all.values()); // iterator is thread safe
+ }
+
+ Collection<Task<?>> 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<V> implements RunnableScheduledFuture<V> {
+ /**
+ * Summarized status of a single task.
+ * <p>
+ * Tasks have the following state flow:
+ * <ol>
+ * <li>{@link #SLEEPING}: if scheduled with a non-zero delay.</li>
+ * <li>{@link #READY}: waiting for an available worker thread.</li>
+ * <li>{@link #RUNNING}: actively executing on a worker thread.</li>
+ * <li>{@link #DONE}: finished executing, if not periodic.</li>
+ * </ol>
+ */
+ 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<V> task;
+ private final Executor executor;
+ private final int taskId;
+ private final AtomicBoolean running;
+ private final Date startTime;
+
+ Task(Runnable runnable, RunnableScheduledFuture<V> 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;
+ }
+
+ @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
+ // 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;
+ }
+ }
+
+ @Override
+ public int compareTo(Delayed o) {
+ return task.compareTo(o);
+ }
+
+ @Override
+ public V get() throws InterruptedException, ExecutionException {
+ return task.get();
+ }
+
+ @Override
+ public V get(long timeout, TimeUnit unit) throws InterruptedException,
+ ExecutionException, TimeoutException {
+ return task.get(timeout, unit);
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return task.getDelay(unit);
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return task.isCancelled();
+ }
+
+ @Override
+ public boolean isDone() {
+ return task.isDone();
+ }
+
+ @Override
+ public boolean isPeriodic() {
+ return task.isPeriodic();
+ }
+
+ @Override
+ 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..e698eb54
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/cli/CmdLineParser.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
+ *
+ * (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.
+ * <p>
+ * 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<String, OptionHandler> 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<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+ 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<String> 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<String, String[]> parameters) throws CmdLineException {
+ Multimap<String, String> map = LinkedHashMultimap.create();
+ for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
+ for (String val : ent.getValue()) {
+ map.put(ent.getKey(), val);
+ }
+ }
+ parseOptionMap(map);
+ }
+
+ public void parseOptionMap(Multimap<String, String> params) throws CmdLineException {
+ List<String> 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<String, OptionHandler> index(List<OptionHandler> in) {
+ Map<String, OptionHandler> 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<OptionHandler> 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 <T> boolean isEnum(Setter<T> setter) {
+ return Enum.class.isAssignableFrom(setter.getType());
+ }
+
+ private <T> boolean isPrimitive(Setter<T> setter) {
+ return setter.getType().isPrimitive();
+ }
+ }
+
+ private static class HelpOption implements Option, Setter<Boolean> {
+ 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<? extends OptionHandler<Boolean>> handler() {
+ return BooleanOptionHandler.class;
+ }
+
+ @Override
+ public String metaVar() {
+ return "";
+ }
+
+ @Override
+ public boolean required() {
+ return false;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return Option.class;
+ }
+
+ @Override
+ public FieldSetter asFieldSetter() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public AnnotatedElement asAnnotatedElement() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Class<Boolean> 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<String> {
+
+ public SubcommandHandler(CmdLineParser parser,
+ OptionDef option, Setter<String> 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/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/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<RepositoryUrl> 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;
}
/**
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
index bcd84b66..938226a6 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
@@ -165,7 +165,7 @@ public class RepositoryUrlPanel extends BasePanel {
if (repository.isMirror) {
urlPanel.add(WicketUtils.newImage("accessRestrictionIcon", "mirror_16x16.png",
getString("gb.isMirror")));
- } else if (app().runtime().isServingRepositories()) {
+ } else if (app().gitblit().isServingRepositories()) {
switch (repository.accessRestriction) {
case NONE:
urlPanel.add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));
@@ -210,7 +210,7 @@ public class RepositoryUrlPanel extends BasePanel {
return urlPanel;
}
- protected Fragment createApplicationMenus(String wicketId, UserModel user, final RepositoryModel repository, final List<RepositoryUrl> repositoryUrls) {
+ protected Fragment createApplicationMenus(String wicketId, final UserModel user, final RepositoryModel repository, final List<RepositoryUrl> repositoryUrls) {
final List<GitClientApplication> displayedApps = new ArrayList<GitClientApplication>();
final String userAgent = ((WebClientInfo) GitBlitWebSession.get().getClientInfo()).getUserAgent();
@@ -309,13 +309,13 @@ public class RepositoryUrlPanel extends BasePanel {
if (!StringUtils.isEmpty(clientApp.cloneUrl)) {
// custom registered url
- String url = substitute(clientApp.cloneUrl, repoUrl.url, baseURL);
+ String url = substitute(clientApp.cloneUrl, repoUrl.url, baseURL, user.username, repository.name);
fragment.add(new LinkPanel("content", "applicationMenuItem", getString("gb.clone") + " " + repoUrl.url, url));
repoLinkItem.add(fragment);
fragment.add(new Label("copyFunction").setVisible(false));
} else if (!StringUtils.isEmpty(clientApp.command)) {
// command-line
- String command = substitute(clientApp.command, repoUrl.url, baseURL);
+ String command = substitute(clientApp.command, repoUrl.url, baseURL, user.username, repository.name);
Label content = new Label("content", command);
WicketUtils.setCssClass(content, "commandMenuItem");
fragment.add(content);
@@ -334,8 +334,8 @@ public class RepositoryUrlPanel extends BasePanel {
return applicationMenus;
}
- protected String substitute(String pattern, String repoUrl, String baseUrl) {
- return pattern.replace("${repoUrl}", repoUrl).replace("${baseUrl}", baseUrl);
+ protected String substitute(String pattern, String repoUrl, String baseUrl, String username, String repository) {
+ return pattern.replace("${repoUrl}", repoUrl).replace("${baseUrl}", baseUrl).replace("${username}", username).replace("${repository}", repository);
}
protected Label createPermissionBadge(String wicketId, RepositoryUrl repoUrl) {
diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties
index c6b5d8c3..43d31d80 100644
--- a/src/main/java/log4j.properties
+++ b/src/main/java/log4j.properties
@@ -25,6 +25,9 @@ log4j.rootCategory=INFO, S
#log4j.logger.net=INFO
#log4j.logger.com.gitblit=DEBUG
+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/site/design.mkd b/src/site/design.mkd
index 6d4b29ca..cd4b1b71 100644
--- a/src/site/design.mkd
+++ b/src/site/design.mkd
@@ -55,6 +55,8 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread
- [commons-codec](http://commons.apache.org/proper/commons-codec) (Apache 2.0)
- [pegdown](https://github.com/sirthias/pegdown) (Apache 2.0)
- [jedis](https://github.com/xetorthio/jedis) (MIT)
+- [Mina SSHD](https://mina.apache.org) (Apache 2.0)
+- [pf4j](https://github.com/decebals/pf4j) (Apache 2.0)
### Other Build Dependencies
- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
diff --git a/src/site/faq.mkd b/src/site/faq.mkd
index a631797a..1b522f82 100644
--- a/src/site/faq.mkd
+++ b/src/site/faq.mkd
@@ -111,7 +111,7 @@ Yes. You can manually manipulate all of them and (most) changes will be immedia
Care must be taken to preserve the relationship between user roles and repository names.<br/>Please see the *User Roles* section of the [setup](/setup.html) page for details.
### Can I restrict access to branches or paths within a repository?
-No, not out-of-the-box. Access restrictions apply to the repository as a whole.
+No, not yet. Access restrictions apply to the repository as a whole.
Gitblit's simple authentication and authorization mechanism can be used to facilitate one or more of the [workflows outlined here](http://progit.org/book/ch5-1.html).
@@ -122,10 +122,6 @@ Alternatively, you could use [gitolite](https://github.com/sitaramc/gitolite) an
### Can I authenticate users against XYZ?
Yes. The user service is pluggable. You may write your own complete user service by implementing the *com.gitblit.IUserService* interface. Or you may subclass *com.gitblit.GitblitUserService* and override just the authentication. Set the fully qualified classname as the *realm.userService* property.
-### Why doesn't Gitblit support SSH?
-
-It will. This feature is in development and should land in the 1.5.0 release.
-
### What types of Search does Gitblit support?
As of 0.9.0, Gitblit supports Lucene-based searching.
diff --git a/src/site/features.mkd b/src/site/features.mkd
index 2d3daa56..dc048804 100644
--- a/src/site/features.mkd
+++ b/src/site/features.mkd
@@ -1,6 +1,7 @@
## Standard Features (GO/WAR)
-- JGit http/https SmartHTTP servlet
-- JGit git protocol daemon
+- Integrated JGit http/https SmartHTTP servlet
+- Integrated JGit git protocol daemon
+- Integrated Mina SSHD daemon
- Optional feature to allow users to create personal repositories
- Optional feature to fork a repository to a personal repository
- Optional feature to create a repository on push
@@ -20,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
@@ -45,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
@@ -77,7 +79,6 @@
- Built-in AJP connector for Apache httpd
## Limitations
-- HTTP/HTTPS/GIT are the only supported Git protocols (SSH is in progress, ticket-6)
- Built-in access controls are not branch-based, they are repository-based.
[jgit]: http://eclipse.org/jgit "Eclipse JGit Site"
diff --git a/src/site/resources/6x12.dfont b/src/site/resources/6x12.dfont
new file mode 100644
index 00000000..8c35f35a
--- /dev/null
+++ b/src/site/resources/6x12.dfont
Binary files differ
diff --git a/src/site/resources/6x13.dfont b/src/site/resources/6x13.dfont
new file mode 100644
index 00000000..6cf59fe9
--- /dev/null
+++ b/src/site/resources/6x13.dfont
Binary files differ
diff --git a/src/site/resources/7x13.dfont b/src/site/resources/7x13.dfont
new file mode 100644
index 00000000..577d638b
--- /dev/null
+++ b/src/site/resources/7x13.dfont
Binary files differ
diff --git a/src/site/resources/7x14.dfont b/src/site/resources/7x14.dfont
new file mode 100644
index 00000000..465af8ea
--- /dev/null
+++ b/src/site/resources/7x14.dfont
Binary files differ
diff --git a/src/site/setup_plugins.mkd b/src/site/setup_plugins.mkd
new file mode 100644
index 00000000..b609a683
--- /dev/null
+++ b/src/site/setup_plugins.mkd
@@ -0,0 +1,72 @@
+
+## Gitblit Plugins
+
+*SINCE 1.5.0*
+
+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
+
+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.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
+
+**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.
+
+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)
+
diff --git a/src/site/setup_client.mkd b/src/site/setup_transport_http.mkd
index d8fc7d14..fd611d43 100644
--- a/src/site/setup_client.mkd
+++ b/src/site/setup_transport_http.mkd
@@ -1,5 +1,6 @@
-## Client Setup and Configuration
+## 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.
@@ -42,6 +43,3 @@ error: error:14077458:SSL routines:SSL23_GET_SERVER_HELLO:reason(1112) while acc
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_ssh.mkd b/src/site/setup_transport_ssh.mkd
new file mode 100644
index 00000000..a671e5af
--- /dev/null
+++ b/src/site/setup_transport_ssh.mkd
@@ -0,0 +1,86 @@
+
+## 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://<username>@<hostname>: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 <username> -p 29418 <hostname> keys add
+ cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> 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.
+
+Once you've done both of those steps you should be able to execute the following command without a password prompt.
+
+ ssh -l <username> -p 29418 <hostname>
+
+### Setting up an SSH alias
+
+Typing the following command syntax all the time gets to be rather tedious.
+
+ ssh -l <username> -p 29418 <hostname>
+
+You can define an alias for your server which will reduce your command syntax to something like this.
+
+ ssh <alias>
+
+Create or modify your `~/.ssh/config` file and add a host entry. If you are on Windows, you'll want to create or modify `<userfolder>\.ssh\config`, where *userfolder* is dependent on your version of Windows. Most recently this is `c:\users\<userfolder>`.
+
+ Host <alias>
+ IdentityFile ~/.ssh/id_rsa
+ User <username>
+ Port 29418
+ HostName <hostname>
+
+### SSH Commands
+
+Gitblit supports SSH command plugins and provides several commands out-of-the-box.
+
+#### keys
+
+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 <username> -p 29418 <hostname> keys add
+
+##### keys list
+
+Show the SSH public keys you have added to your account.
+
+ ssh -l <username> -p 29418 <hostname> 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 <username> -p 29418 <hostname> keys remove 2
+
+You can also remove all your public keys from your account.
+
+ ssh -l <username> -p 29418 <hostname> keys remove ALL
+
+### SSH Command Plugins
+
+Gitblit supports loading custom SSH command plugins.
+
diff --git a/src/test/config/test-gitblit.properties b/src/test/config/test-gitblit.properties
index e636469e..1a52eaf4 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.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/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index c015c847..5a7dcea1 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -61,10 +61,11 @@ 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, 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");
@@ -78,10 +79,12 @@ public class GitBlitSuite {
static int port = 8280;
static int gitPort = 8300;
static int shutdownPort = 8281;
+ 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";
@@ -135,10 +138,15 @@ 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(),
+ 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/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
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..dcaeaff8
--- /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.File;
+import java.text.MessageFormat;
+import java.util.List;
+
+import org.apache.sshd.ClientSession;
+import org.apache.sshd.SshClient;
+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.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.JGitUtils;
+
+public class SshDaemonTest extends SshUnitTest {
+
+ static File ticgitFolder = new File(GitBlitSuite.REPOSITORIES, "working/ticgit");
+
+ String url = GitBlitSuite.sshDaemonUrl;
+
+ @Test
+ public void testPublicKeyAuthentication() throws Exception {
+ 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 {
+ String result = testSshCommand("version");
+ assertEquals(Constants.getGitBlitVersion(), result);
+ }
+
+ @Test
+ public void testCloneCommand() throws Exception {
+ if (ticgitFolder.exists()) {
+ GitBlitSuite.close(ticgitFolder);
+ FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE);
+ }
+
+ // 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<RevCommit> 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<SshKey> 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<SshKey> 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<SshKey> 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<SshKey> 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<SshKey> 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<SshKey> 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<SshKey> 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;
+ }
+}