James Moger
2012-11-06 798581cab5817310a1b9991dac3b10cd7813f86a
commit | author | age
f13c4c 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
5fe7df 16 package com.gitblit;
JM 17
18 import java.io.BufferedReader;
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.InputStreamReader;
22 import java.io.OutputStream;
23 import java.net.InetAddress;
24 import java.net.ServerSocket;
25 import java.net.Socket;
6f0d84 26 import java.net.URI;
5fe7df 27 import java.net.URL;
JM 28 import java.net.UnknownHostException;
29 import java.security.ProtectionDomain;
87cc1e 30 import java.text.MessageFormat;
5fe7df 31 import java.util.ArrayList;
JM 32 import java.util.List;
f3b625 33 import java.util.Scanner;
5fe7df 34
4b9d64 35 import org.eclipse.jetty.ajp.Ajp13SocketConnector;
5fe7df 36 import org.eclipse.jetty.server.Connector;
JM 37 import org.eclipse.jetty.server.Server;
38 import org.eclipse.jetty.server.bio.SocketConnector;
39 import org.eclipse.jetty.server.nio.SelectChannelConnector;
a4d249 40 import org.eclipse.jetty.server.session.HashSessionManager;
5fe7df 41 import org.eclipse.jetty.server.ssl.SslConnector;
JM 42 import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
43 import org.eclipse.jetty.server.ssl.SslSocketConnector;
44 import org.eclipse.jetty.util.thread.QueuedThreadPool;
45 import org.eclipse.jetty.webapp.WebAppContext;
892570 46 import org.eclipse.jgit.util.FileUtils;
2a7306 47 import org.slf4j.Logger;
JM 48 import org.slf4j.LoggerFactory;
5fe7df 49
JM 50 import com.beust.jcommander.JCommander;
51 import com.beust.jcommander.Parameter;
52 import com.beust.jcommander.ParameterException;
53 import com.beust.jcommander.Parameters;
87cc1e 54 import com.gitblit.utils.StringUtils;
f3b625 55 import com.unboundid.ldap.listener.InMemoryDirectoryServer;
JC 56 import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
57 import com.unboundid.ldap.listener.InMemoryListenerConfig;
58 import com.unboundid.ldif.LDIFReader;
5fe7df 59
892570 60 /**
JM 61  * GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts
62  * and stops an instance of Jetty that is configured from a combination of the
63  * gitblit.properties file and command line parameters. JCommander is used to
64  * simplify command line parameter processing. This class also automatically
65  * generates a self-signed certificate for localhost, if the keystore does not
66  * already exist.
67  * 
68  * @author James Moger
69  * 
70  */
5fe7df 71 public class GitBlitServer {
JM 72
2a7306 73     private static Logger logger;
5fe7df 74
a2709d 75     public static void main(String... args) {
5fe7df 76         Params params = new Params();
JM 77         JCommander jc = new JCommander(params);
78         try {
79             jc.parse(args);
2a7306 80             if (params.help) {
5fe7df 81                 usage(jc, null);
2a7306 82             }
5fe7df 83         } catch (ParameterException t) {
JM 84             usage(jc, t);
85         }
86
2a7306 87         if (params.stop) {
5fe7df 88             stop(params);
2a7306 89         } else {
5fe7df 90             start(params);
2a7306 91         }
5fe7df 92     }
JM 93
892570 94     /**
JM 95      * Display the command line usage of Gitblit GO.
96      * 
97      * @param jc
98      * @param t
99      */
5fe7df 100     private static void usage(JCommander jc, ParameterException t) {
1f9dae 101         System.out.println(Constants.BORDER);
2a7306 102         System.out.println(Constants.getGitBlitVersion());
1f9dae 103         System.out.println(Constants.BORDER);
5fe7df 104         System.out.println();
JM 105         if (t != null) {
106             System.out.println(t.getMessage());
107             System.out.println();
108         }
109         if (jc != null) {
110             jc.usage();
2a7306 111             System.out
892570 112                     .println("\nExample:\n  java -server -Xmx1024M -jar gitblit.jar --repositoriesFolder c:\\git --httpPort 80 --httpsPort 443");
5fe7df 113         }
JM 114         System.exit(0);
115     }
116
117     /**
892570 118      * Stop Gitblt GO.
5fe7df 119      */
JM 120     public static void stop(Params params) {
121         try {
122             Socket s = new Socket(InetAddress.getByName("127.0.0.1"), params.shutdownPort);
123             OutputStream out = s.getOutputStream();
124             System.out.println("Sending Shutdown Request to " + Constants.NAME);
2a7306 125             out.write("\r\n".getBytes());
5fe7df 126             out.flush();
JM 127             s.close();
128         } catch (UnknownHostException e) {
129             e.printStackTrace();
130         } catch (IOException e) {
131             e.printStackTrace();
132         }
133     }
134
135     /**
892570 136      * Start Gitblit GO.
5fe7df 137      */
JM 138     private static void start(Params params) {
85c2e6 139         FileSettings settings = Params.FILESETTINGS;
7e8873 140         if (!StringUtils.isEmpty(params.settingsfile)) {
JM 141             if (new File(params.settingsfile).exists()) {
142                 settings = new FileSettings(params.settingsfile);                
143             }
144         }
5fe7df 145
2a7306 146         logger = LoggerFactory.getLogger(GitBlitServer.class);
1f9dae 147         logger.info(Constants.BORDER);
2a02f8 148         logger.info("            _____  _  _    _      _  _  _");
JM 149         logger.info("           |  __ \\(_)| |  | |    | |(_)| |");
150         logger.info("           | |  \\/ _ | |_ | |__  | | _ | |_");
151         logger.info("           | | __ | || __|| '_ \\ | || || __|");
152         logger.info("           | |_\\ \\| || |_ | |_) || || || |_");
153         logger.info("            \\____/|_| \\__||_.__/ |_||_| \\__|");
154         int spacing = (Constants.BORDER.length() - Constants.getGitBlitVersion().length()) / 2;
155         StringBuilder sb = new StringBuilder();
156         while (spacing > 0) {
157             spacing--;
158             sb.append(' ');
159         }
160         logger.info(sb.toString() + Constants.getGitBlitVersion());
161         logger.info("");
1f9dae 162         logger.info(Constants.BORDER);
5fe7df 163
eb7609 164         System.setProperty("java.awt.headless", "true");
JM 165
5fe7df 166         String osname = System.getProperty("os.name");
JM 167         String osversion = System.getProperty("os.version");
168         logger.info("Running on " + osname + " (" + osversion + ")");
eb7609 169         
5fe7df 170         List<Connector> connectors = new ArrayList<Connector>();
892570 171
JM 172         // conditionally configure the http connector
5fe7df 173         if (params.port > 0) {
JM 174             Connector httpConnector = createConnector(params.useNIO, params.port);
1f9dae 175             String bindInterface = settings.getString(Keys.server.httpBindInterface, null);
87cc1e 176             if (!StringUtils.isEmpty(bindInterface)) {
f28063 177                 logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}",
2a7306 178                         params.port, bindInterface));
87cc1e 179                 httpConnector.setHost(bindInterface);
JM 180             }
18422e 181             if (params.port < 1024 && !isWindows()) {
JM 182                 logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
183             }
5fe7df 184             connectors.add(httpConnector);
JM 185         }
7ba0ec 186
892570 187         // conditionally configure the https connector
5fe7df 188         if (params.securePort > 0) {
5b60db 189             File keystore = new File("keystore");
JM 190             if (!keystore.exists()) {
892570 191                 logger.info("Generating self-signed SSL certificate for localhost");
2a7306 192                 MakeCertificate.generateSelfSignedCertificate("localhost", keystore,
JM 193                         params.storePassword);
5b60db 194             }
JM 195             if (keystore.exists()) {
2a7306 196                 Connector secureConnector = createSSLConnector(keystore, params.storePassword,
JM 197                         params.useNIO, params.securePort);
1f9dae 198                 String bindInterface = settings.getString(Keys.server.httpsBindInterface, null);
87cc1e 199                 if (!StringUtils.isEmpty(bindInterface)) {
2a02f8 200                     logger.warn(MessageFormat.format(
JM 201                             "Binding ssl connector on port {0,number,0} to {1}", params.securePort,
202                             bindInterface));
87cc1e 203                     secureConnector.setHost(bindInterface);
JM 204                 }
18422e 205                 if (params.securePort < 1024 && !isWindows()) {
JM 206                     logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
88598b 207                 }
5fe7df 208                 connectors.add(secureConnector);
JM 209             } else {
5b60db 210                 logger.warn("Failed to find or load Keystore?");
5fe7df 211                 logger.warn("SSL connector DISABLED.");
JM 212             }
213         }
214
4b9d64 215         // conditionally configure the ajp connector
JM 216         if (params.ajpPort > 0) {
217             Connector ajpConnector = createAJPConnector(params.ajpPort);
218             String bindInterface = settings.getString(Keys.server.ajpBindInterface, null);
219             if (!StringUtils.isEmpty(bindInterface)) {
220                 logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}",
221                         params.ajpPort, bindInterface));
222                 ajpConnector.setHost(bindInterface);
223             }
224             if (params.ajpPort < 1024 && !isWindows()) {
225                 logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
226             }
227             connectors.add(ajpConnector);
228         }
229
892570 230         // tempDir is where the embedded Gitblit web application is expanded and
JM 231         // where Jetty creates any necessary temporary files
5fe7df 232         File tempDir = new File(params.temp);
2a7306 233         if (tempDir.exists()) {
892570 234             try {
JM 235                 FileUtils.delete(tempDir, FileUtils.RECURSIVE | FileUtils.RETRY);
236             } catch (IOException x) {
237                 logger.warn("Failed to delete temp dir " + tempDir.getAbsolutePath(), x);
2a7306 238             }
JM 239         }
240         if (!tempDir.mkdirs()) {
241             logger.warn("Failed to create temp dir " + tempDir.getAbsolutePath());
242         }
5fe7df 243
JM 244         Server server = new Server();
245         server.setStopAtShutdown(true);
246         server.setConnectors(connectors.toArray(new Connector[connectors.size()]));
247
248         // Get the execution path of this class
249         // We use this to set the WAR path.
250         ProtectionDomain protectionDomain = GitBlitServer.class.getProtectionDomain();
251         URL location = protectionDomain.getCodeSource().getLocation();
252
253         // Root WebApp Context
254         WebAppContext rootContext = new WebAppContext();
5d253a 255         rootContext.setContextPath(settings.getString(Keys.server.contextPath, "/"));
5fe7df 256         rootContext.setServer(server);
JM 257         rootContext.setWar(location.toExternalForm());
258         rootContext.setTempDirectory(tempDir);
2a7306 259
JM 260         // Set cookies HttpOnly so they are not accessible to JavaScript engines
a4d249 261         HashSessionManager sessionManager = new HashSessionManager();
JM 262         sessionManager.setHttpOnly(true);
263         // Use secure cookies if only serving https
264         sessionManager.setSecureCookies(params.port <= 0 && params.securePort > 0);
265         rootContext.getSessionHandler().setSessionManager(sessionManager);
8c9a20 266
85c2e6 267         // Ensure there is a defined User Service
JM 268         String realmUsers = params.userService;
8c9a20 269         if (StringUtils.isEmpty(realmUsers)) {
85c2e6 270             logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService));
8c9a20 271             return;
JM 272         }
85c2e6 273
892570 274         // Override settings from the command-line
85c2e6 275         settings.overrideSetting(Keys.realm.userService, params.userService);
0fe70c 276         settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder);
f3b625 277         
JC 278         // Start up an in-memory LDAP server, if configured
279         try {
280             if (StringUtils.isEmpty(params.ldapLdifFile) == false) {
281                 File ldifFile = new File(params.ldapLdifFile);
282                 if (ldifFile != null && ldifFile.exists()) {
668663 283                     URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
f3b625 284                     String firstLine = new Scanner(ldifFile).nextLine();
JC 285                     String rootDN = firstLine.substring(4);
668663 286                     String bindUserName = settings.getString(Keys.realm.ldap.username, "");
JM 287                     String bindPassword = settings.getString(Keys.realm.ldap.password, "");
f3b625 288                     
6f0d84 289                     // Get the port
JC 290                     int port = ldapUrl.getPort();
291                     if (port == -1)
292                         port = 389;
293                     
f3b625 294                     InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN);
JC 295                     config.addAdditionalBindCredentials(bindUserName, bindPassword);
6f0d84 296                     config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", port));
f3b625 297                     config.setSchema(null);
JC 298                     
299                     InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
300                     ds.importFromLDIF(true, new LDIFReader(ldifFile));
301                     ds.startListening();
302                     
6f0d84 303                     logger.info("LDAP Server started at ldap://localhost:" + port);
f3b625 304                 }
JC 305             }
306         } catch (Exception e) {
307             // Completely optional, just show a warning
308             logger.warn("Unable to start LDAP server", e);
309         }
db653a 310
5fe7df 311         // Set the server's contexts
8c9a20 312         server.setHandler(rootContext);
87cc1e 313
JM 314         // Setup the GitBlit context
315         GitBlit gitblit = GitBlit.self();
f6740d 316         gitblit.configureContext(settings, true);
87cc1e 317         rootContext.addEventListener(gitblit);
5fe7df 318
JM 319         try {
892570 320             // start the shutdown monitor
5fe7df 321             if (params.shutdownPort > 0) {
JM 322                 Thread shutdownMonitor = new ShutdownMonitorThread(server, params);
323                 shutdownMonitor.start();
324             }
892570 325
JM 326             // start Jetty
5fe7df 327             server.start();
JM 328             server.join();
329         } catch (Exception e) {
330             e.printStackTrace();
331             System.exit(100);
332         }
333     }
334
892570 335     /**
JM 336      * Creates an http connector.
337      * 
338      * @param useNIO
339      * @param port
18422e 340      * @return an http connector
892570 341      */
5fe7df 342     private static Connector createConnector(boolean useNIO, int port) {
JM 343         Connector connector;
344         if (useNIO) {
345             logger.info("Setting up NIO SelectChannelConnector on port " + port);
346             SelectChannelConnector nioconn = new SelectChannelConnector();
347             nioconn.setSoLingerTime(-1);
348             nioconn.setThreadPool(new QueuedThreadPool(20));
349             connector = nioconn;
350         } else {
351             logger.info("Setting up SocketConnector on port " + port);
352             SocketConnector sockconn = new SocketConnector();
353             connector = sockconn;
354         }
355
356         connector.setPort(port);
357         connector.setMaxIdleTime(30000);
358         return connector;
359     }
360
892570 361     /**
JM 362      * Creates an https connector.
363      * 
c7ebb2 364      * SSL renegotiation will be enabled if the JVM is 1.6.0_22 or later.
JM 365      * oracle.com/technetwork/java/javase/documentation/tlsreadme2-176330.html
366      * 
892570 367      * @param keystore
JM 368      * @param password
369      * @param useNIO
370      * @param port
371      * @return an https connector
372      */
2a7306 373     private static Connector createSSLConnector(File keystore, String password, boolean useNIO,
JM 374             int port) {
5fe7df 375         SslConnector connector;
JM 376         if (useNIO) {
377             logger.info("Setting up NIO SslSelectChannelConnector on port " + port);
378             SslSelectChannelConnector ssl = new SslSelectChannelConnector();
379             ssl.setSoLingerTime(-1);
2a7306 380             ssl.setThreadPool(new QueuedThreadPool(20));
5fe7df 381             connector = ssl;
JM 382         } else {
383             logger.info("Setting up NIO SslSocketConnector on port " + port);
384             SslSocketConnector ssl = new SslSocketConnector();
385             connector = ssl;
386         }
c7ebb2 387         // disable renegotiation unless this is a patched JVM
JM 388         boolean allowRenegotiation = false;
389         String v = System.getProperty("java.version");
390         if (v.startsWith("1.7")) {
391             allowRenegotiation = true;
392         } else if (v.startsWith("1.6")) {
393             // 1.6.0_22 was first release with RFC-5746 implemented fix.
394             if (v.indexOf('_') > -1) {
395                 String b = v.substring(v.indexOf('_') + 1);
396                 if (Integer.parseInt(b) >= 22) {
397                     allowRenegotiation = true;
398                 }
399             }
400         }
401         if (allowRenegotiation) {
402             logger.info("   allowing SSL renegotiation on Java " + v);
f28063 403             connector.setAllowRenegotiate(allowRenegotiation);
2a02f8 404         }
5b60db 405         connector.setKeystore(keystore.getAbsolutePath());
5fe7df 406         connector.setPassword(password);
JM 407         connector.setPort(port);
408         connector.setMaxIdleTime(30000);
409         return connector;
4b9d64 410     }
JM 411     
412     /**
413      * Creates an ajp connector.
414      * 
415      * @param port
416      * @return an ajp connector
417      */
418     private static Connector createAJPConnector(int port) {
419         logger.info("Setting up AJP Connector on port " + port);
420         Ajp13SocketConnector ajp = new Ajp13SocketConnector();
421         ajp.setPort(port);
422         if (port < 1024 && !isWindows()) {
423             logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
424         }
425         return ajp;
5fe7df 426     }
88598b 427
18422e 428     /**
JM 429      * Tests to see if the operating system is Windows.
430      * 
431      * @return true if this is a windows machine
432      */
433     private static boolean isWindows() {
434         return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1;
435     }
2a7306 436
5fe7df 437     /**
892570 438      * The ShutdownMonitorThread opens a socket on a specified port and waits
JM 439      * for an incoming connection. When that connection is accepted a shutdown
440      * message is issued to the running Jetty server.
5fe7df 441      * 
892570 442      * @author James Moger
JM 443      * 
5fe7df 444      */
JM 445     private static class ShutdownMonitorThread extends Thread {
446
447         private final ServerSocket socket;
448
449         private final Server server;
db653a 450
1f9dae 451         private final Logger logger = LoggerFactory.getLogger(ShutdownMonitorThread.class);
5fe7df 452
JM 453         public ShutdownMonitorThread(Server server, Params params) {
454             this.server = server;
455             setDaemon(true);
456             setName(Constants.NAME + " Shutdown Monitor");
457             ServerSocket skt = null;
458             try {
459                 skt = new ServerSocket(params.shutdownPort, 1, InetAddress.getByName("127.0.0.1"));
460             } catch (Exception e) {
2a7306 461                 logger.warn("Could not open shutdown monitor on port " + params.shutdownPort, e);
5fe7df 462             }
JM 463             socket = skt;
464         }
465
466         @Override
467         public void run() {
468             logger.info("Shutdown Monitor listening on port " + socket.getLocalPort());
469             Socket accept;
470             try {
471                 accept = socket.accept();
2a7306 472                 BufferedReader reader = new BufferedReader(new InputStreamReader(
JM 473                         accept.getInputStream()));
5fe7df 474                 reader.readLine();
1f9dae 475                 logger.info(Constants.BORDER);
5fe7df 476                 logger.info("Stopping " + Constants.NAME);
1f9dae 477                 logger.info(Constants.BORDER);
5fe7df 478                 server.stop();
JM 479                 server.setStopAtShutdown(false);
480                 accept.close();
481                 socket.close();
482             } catch (Exception e) {
483                 logger.warn("Failed to shutdown Jetty", e);
484             }
485         }
486     }
487
88598b 488     /**
JM 489      * JCommander Parameters class for GitBlitServer.
490      */
5fe7df 491     @Parameters(separators = " ")
JM 492     private static class Params {
db653a 493
28d6b2 494         private static final FileSettings FILESETTINGS = new FileSettings(Constants.PROPERTIES_FILE);
5fe7df 495
JM 496         /*
497          * Server parameters
498          */
499         @Parameter(names = { "-h", "--help" }, description = "Show this help")
500         public Boolean help = false;
501
502         @Parameter(names = { "--stop" }, description = "Stop Server")
503         public Boolean stop = false;
504
85c2e6 505         @Parameter(names = { "--tempFolder" }, description = "Folder for server to extract built-in webapp")
2a7306 506         public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp");
5fe7df 507
JM 508         /*
509          * GIT Servlet Parameters
510          */
dfb889 511         @Parameter(names = { "--repositoriesFolder" }, description = "Git Repositories Folder")
2a7306 512         public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder,
JM 513                 "repos");
5fe7df 514
JM 515         /*
516          * Authentication Parameters
517          */
85c2e6 518         @Parameter(names = { "--userService" }, description = "Authentication and Authorization Service (filename or fully qualified classname)")
892570 519         public String userService = FILESETTINGS.getString(Keys.realm.userService,
JM 520                 "users.properties");
5fe7df 521
JM 522         /*
523          * JETTY Parameters
524          */
dfb889 525         @Parameter(names = { "--useNio" }, description = "Use NIO Connector else use Socket Connector.")
2a7306 526         public Boolean useNIO = FILESETTINGS.getBoolean(Keys.server.useNio, true);
5fe7df 527
dfb889 528         @Parameter(names = "--httpPort", description = "HTTP port for to serve. (port <= 0 will disable this connector)")
2a7306 529         public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 80);
5fe7df 530
dfb889 531         @Parameter(names = "--httpsPort", description = "HTTPS port to serve.  (port <= 0 will disable this connector)")
2a7306 532         public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 443);
5fe7df 533
4b9d64 534         @Parameter(names = "--ajpPort", description = "AJP port to serve.  (port <= 0 will disable this connector)")
JM 535         public Integer ajpPort = FILESETTINGS.getInteger(Keys.server.ajpPort, 0);
536
5fe7df 537         @Parameter(names = "--storePassword", description = "Password for SSL (https) keystore.")
2a7306 538         public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, "");
5fe7df 539
JM 540         @Parameter(names = "--shutdownPort", description = "Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)")
2a7306 541         public Integer shutdownPort = FILESETTINGS.getInteger(Keys.server.shutdownPort, 8081);
5fe7df 542
7e8873 543         /*
JM 544          * Setting overrides
545          */
546         @Parameter(names = { "--settings" }, description = "Path to alternative settings")
547         public String settingsfile;
f3b625 548         
JC 549         @Parameter(names = { "--ldapLdifFile" }, description = "Path to LDIF file.  This will cause an in-memory LDAP server to be started according to gitblit settings")
550         public String ldapLdifFile;
7e8873 551
5fe7df 552     }
JM 553 }