James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
commit | author | age
269c50 1 /*
JM 2  * Copyright 2013 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  */
16 package com.gitblit.manager;
17
18 import java.io.IOException;
58baee 19 import java.net.URI;
269c50 20 import java.text.MessageFormat;
7d3a31 21 import java.util.ArrayList;
269c50 22 import java.util.Arrays;
7d3a31 23 import java.util.Collections;
JM 24 import java.util.Comparator;
269c50 25 import java.util.Date;
7d3a31 26 import java.util.HashSet;
JM 27 import java.util.Iterator;
269c50 28 import java.util.List;
7d3a31 29 import java.util.Set;
269c50 30 import java.util.concurrent.Executors;
JM 31 import java.util.concurrent.ScheduledExecutorService;
32 import java.util.concurrent.TimeUnit;
33
3a9e76 34 import javax.servlet.http.HttpServletRequest;
JM 35
269c50 36 import org.slf4j.Logger;
JM 37 import org.slf4j.LoggerFactory;
38
7d3a31 39 import com.gitblit.Constants;
3a9e76 40 import com.gitblit.Constants.AccessPermission;
JM 41 import com.gitblit.Constants.AccessRestrictionType;
269c50 42 import com.gitblit.Constants.FederationToken;
7d3a31 43 import com.gitblit.Constants.Transport;
269c50 44 import com.gitblit.IStoredSettings;
JM 45 import com.gitblit.Keys;
46 import com.gitblit.fanout.FanoutNioService;
47 import com.gitblit.fanout.FanoutService;
48 import com.gitblit.fanout.FanoutSocketService;
49 import com.gitblit.models.FederationModel;
3a9e76 50 import com.gitblit.models.RepositoryModel;
7d3a31 51 import com.gitblit.models.RepositoryUrl;
3a9e76 52 import com.gitblit.models.UserModel;
7bf6e1 53 import com.gitblit.service.FederationPullService;
31f477 54 import com.gitblit.transport.git.GitDaemon;
41124c 55 import com.gitblit.transport.ssh.SshDaemon;
7d3a31 56 import com.gitblit.utils.HttpUtils;
269c50 57 import com.gitblit.utils.StringUtils;
JM 58 import com.gitblit.utils.TimeUtils;
5bb55f 59 import com.gitblit.utils.WorkQueue;
7d3a31 60 import com.google.inject.Inject;
JM 61 import com.google.inject.Provider;
62 import com.google.inject.Singleton;
269c50 63
JM 64 /**
65  * Services manager manages long-running services/processes that either have no
66  * direct relation to other managers OR require really high-level manager
67  * integration (i.e. a Gitblit instance).
68  *
69  * @author James Moger
70  *
71  */
7d3a31 72 @Singleton
JM 73 public class ServicesManager implements IServicesManager {
269c50 74
JM 75     private final Logger logger = LoggerFactory.getLogger(getClass());
76
77     private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
78
7d3a31 79     private final Provider<WorkQueue> workQueueProvider;
JM 80
269c50 81     private final IStoredSettings settings;
JM 82
23e08c 83     private final IGitblit gitblit;
5bb55f 84
269c50 85     private FanoutService fanoutService;
JM 86
87     private GitDaemon gitDaemon;
88
41124c 89     private SshDaemon sshDaemon;
EM 90
7d3a31 91     @Inject
JM 92     public ServicesManager(
93             Provider<WorkQueue> workQueueProvider,
94             IStoredSettings settings,
95             IGitblit gitblit) {
96
97         this.workQueueProvider = workQueueProvider;
98
99         this.settings = settings;
269c50 100         this.gitblit = gitblit;
JM 101     }
102
103     @Override
104     public ServicesManager start() {
105         configureFederation();
106         configureFanout();
107         configureGitDaemon();
41124c 108         configureSshDaemon();
269c50 109
JM 110         return this;
111     }
112
113     @Override
114     public ServicesManager stop() {
115         scheduledExecutor.shutdownNow();
116         if (fanoutService != null) {
117             fanoutService.stop();
118         }
119         if (gitDaemon != null) {
120             gitDaemon.stop();
924c9b 121         }
JM 122         if (sshDaemon != null) {
123             sshDaemon.stop();
269c50 124         }
7d3a31 125         workQueueProvider.get().stop();
269c50 126         return this;
JM 127     }
128
7d3a31 129     protected String getRepositoryUrl(HttpServletRequest request, String username, RepositoryModel repository) {
JM 130         String gitblitUrl = settings.getString(Keys.web.canonicalUrl, null);
131         if (StringUtils.isEmpty(gitblitUrl)) {
132             gitblitUrl = HttpUtils.getGitblitURL(request);
133         }
134         StringBuilder sb = new StringBuilder();
135         sb.append(gitblitUrl);
136         sb.append(Constants.R_PATH);
137         sb.append(repository.name);
138
139         // inject username into repository url if authentication is required
140         if (repository.accessRestriction.exceeds(AccessRestrictionType.NONE)
141                 && !StringUtils.isEmpty(username)) {
142             sb.insert(sb.indexOf("://") + 3, username + "@");
143         }
144         return sb.toString();
145     }
146
147     /**
148      * Returns a list of repository URLs and the user access permission.
149      *
150      * @param request
151      * @param user
152      * @param repository
153      * @return a list of repository urls
154      */
155     @Override
156     public List<RepositoryUrl> getRepositoryUrls(HttpServletRequest request, UserModel user, RepositoryModel repository) {
157         if (user == null) {
158             user = UserModel.ANONYMOUS;
159         }
160         String username = StringUtils.encodeUsername(UserModel.ANONYMOUS.equals(user) ? "" : user.username);
161
162         List<RepositoryUrl> list = new ArrayList<RepositoryUrl>();
163
164         // http/https url
c20191 165         if (settings.getBoolean(Keys.git.enableGitServlet, true) &&
JJ 166             settings.getBoolean(Keys.web.showHttpServletUrls, true)) {
7d3a31 167             AccessPermission permission = user.getRepositoryPermission(repository).permission;
JM 168             if (permission.exceeds(AccessPermission.NONE)) {
2db6b3 169                 String repoUrl = getRepositoryUrl(request, username, repository);
JJ 170                 Transport transport = Transport.fromUrl(repoUrl);
67ba8f 171                 if (permission.atLeast(AccessPermission.PUSH) && !acceptsPush(transport)) {
7d3a31 172                     // downgrade the repo permission for this transport
JM 173                     // because it is not an acceptable PUSH transport
174                     permission = AccessPermission.CLONE;
175                 }
2db6b3 176                 list.add(new RepositoryUrl(repoUrl, permission));
7d3a31 177             }
JM 178         }
179
180         // ssh daemon url
181         String sshDaemonUrl = getSshDaemonUrl(request, user, repository);
c20191 182         if (!StringUtils.isEmpty(sshDaemonUrl) &&
JJ 183             settings.getBoolean(Keys.web.showSshDaemonUrls, true)) {
7d3a31 184             AccessPermission permission = user.getRepositoryPermission(repository).permission;
JM 185             if (permission.exceeds(AccessPermission.NONE)) {
67ba8f 186                 if (permission.atLeast(AccessPermission.PUSH) && !acceptsPush(Transport.SSH)) {
7d3a31 187                     // downgrade the repo permission for this transport
JM 188                     // because it is not an acceptable PUSH transport
189                     permission = AccessPermission.CLONE;
190                 }
191
192                 list.add(new RepositoryUrl(sshDaemonUrl, permission));
193             }
194         }
195
196         // git daemon url
197         String gitDaemonUrl = getGitDaemonUrl(request, user, repository);
c20191 198         if (!StringUtils.isEmpty(gitDaemonUrl) &&
JJ 199                 settings.getBoolean(Keys.web.showGitDaemonUrls, true)) {
7d3a31 200             AccessPermission permission = getGitDaemonAccessPermission(user, repository);
JM 201             if (permission.exceeds(AccessPermission.NONE)) {
67ba8f 202                 if (permission.atLeast(AccessPermission.PUSH) && !acceptsPush(Transport.GIT)) {
7d3a31 203                     // downgrade the repo permission for this transport
JM 204                     // because it is not an acceptable PUSH transport
205                     permission = AccessPermission.CLONE;
206                 }
207                 list.add(new RepositoryUrl(gitDaemonUrl, permission));
208             }
209         }
210
211         // add all other urls
212         // {0} = repository
213         // {1} = username
1590fd 214         boolean advertisePermsForOther = settings.getBoolean(Keys.web.advertiseAccessPermissionForOtherUrls, false);
7d3a31 215         for (String url : settings.getStrings(Keys.web.otherUrls)) {
1590fd 216             String externalUrl = null;
JJ 217
7d3a31 218             if (url.contains("{1}")) {
JM 219                 // external url requires username, only add url IF we have one
1590fd 220                 if (StringUtils.isEmpty(username)) {
JJ 221                     continue;
222                 } else {
223                     externalUrl = MessageFormat.format(url, repository.name, username);
7d3a31 224                 }
JM 225             } else {
1590fd 226                 // external url does not require username, just do repo name formatting
JJ 227                 externalUrl = MessageFormat.format(url, repository.name);
7d3a31 228             }
1590fd 229
JJ 230             AccessPermission permission = null;
231             if (advertisePermsForOther) {
232                 permission = user.getRepositoryPermission(repository).permission;
233                 if (permission.exceeds(AccessPermission.NONE)) {
234                     Transport transport = Transport.fromUrl(externalUrl);
235                     if (permission.atLeast(AccessPermission.PUSH) && !acceptsPush(transport)) {
236                         // downgrade the repo permission for this transport
237                         // because it is not an acceptable PUSH transport
238                         permission = AccessPermission.CLONE;
239                     }
240                 }
241             }
242             list.add(new RepositoryUrl(externalUrl, permission));
7d3a31 243         }
JM 244
245         // sort transports by highest permission and then by transport security
246         Collections.sort(list, new Comparator<RepositoryUrl>() {
247
248             @Override
249             public int compare(RepositoryUrl o1, RepositoryUrl o2) {
1590fd 250                 if (o1.hasPermission() && !o2.hasPermission()) {
JJ 251                     // prefer known permission items over unknown
7d3a31 252                     return -1;
1590fd 253                 } else if (!o1.hasPermission() && o2.hasPermission()) {
JJ 254                     // prefer known permission items over unknown
7d3a31 255                     return 1;
1590fd 256                 } else if (!o1.hasPermission() && !o2.hasPermission()) {
7d3a31 257                     // sort by Transport ordinal
JM 258                     return o1.transport.compareTo(o2.transport);
259                 } else if (o1.permission.exceeds(o2.permission)) {
260                     // prefer highest permission
261                     return -1;
262                 } else if (o2.permission.exceeds(o1.permission)) {
263                     // prefer highest permission
264                     return 1;
265                 }
266
267                 // prefer more secure transports
268                 return o1.transport.compareTo(o2.transport);
269             }
270         });
271
272         // consider the user's transport preference
273         RepositoryUrl preferredUrl = null;
274         Transport preferredTransport = user.getPreferences().getTransport();
275         if (preferredTransport != null) {
276             Iterator<RepositoryUrl> itr = list.iterator();
277             while (itr.hasNext()) {
278                 RepositoryUrl url = itr.next();
279                 if (url.transport.equals(preferredTransport)) {
280                     itr.remove();
281                     preferredUrl = url;
282                     break;
283                 }
284             }
285         }
286         if (preferredUrl != null) {
287             list.add(0, preferredUrl);
288         }
289
290         return list;
291     }
292
293     /* (non-Javadoc)
294      * @see com.gitblit.manager.IServicesManager#isServingRepositories()
295      */
296     @Override
8b8799 297     public boolean isServingRepositories() {
67ba8f 298         return isServingHTTPS()
JM 299                 || isServingHTTP()
05f229 300                 || isServingGIT()
JM 301                 || isServingSSH();
302     }
303
7d3a31 304     /* (non-Javadoc)
JM 305      * @see com.gitblit.manager.IServicesManager#isServingHTTP()
306      */
307     @Override
05f229 308     public boolean isServingHTTP() {
67ba8f 309         return settings.getBoolean(Keys.git.enableGitServlet, true)
JM 310                 && ((gitblit.getStatus().isGO && settings.getInteger(Keys.server.httpPort, 0) > 0)
311                         || !gitblit.getStatus().isGO);
312     }
313
314     /* (non-Javadoc)
315      * @see com.gitblit.manager.IServicesManager#isServingHTTPS()
316      */
317     @Override
318     public boolean isServingHTTPS() {
319         return settings.getBoolean(Keys.git.enableGitServlet, true)
320                 && ((gitblit.getStatus().isGO && settings.getInteger(Keys.server.httpsPort, 0) > 0)
321                         || !gitblit.getStatus().isGO);
05f229 322     }
JM 323
7d3a31 324     /* (non-Javadoc)
JM 325      * @see com.gitblit.manager.IServicesManager#isServingGIT()
326      */
327     @Override
05f229 328     public boolean isServingGIT() {
JM 329         return gitDaemon != null && gitDaemon.isRunning();
330     }
331
7d3a31 332     /* (non-Javadoc)
JM 333      * @see com.gitblit.manager.IServicesManager#isServingSSH()
334      */
335     @Override
05f229 336     public boolean isServingSSH() {
JM 337         return sshDaemon != null && sshDaemon.isRunning();
8b8799 338     }
JM 339
269c50 340     protected void configureFederation() {
JM 341         boolean validPassphrase = true;
342         String passphrase = settings.getString(Keys.federation.passphrase, "");
343         if (StringUtils.isEmpty(passphrase)) {
344             logger.info("Federation passphrase is blank! This server can not be PULLED from.");
345             validPassphrase = false;
346         }
347         if (validPassphrase) {
348             // standard tokens
349             for (FederationToken tokenType : FederationToken.values()) {
350                 logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
351                         gitblit.getFederationToken(tokenType)));
352             }
353
354             // federation set tokens
355             for (String set : settings.getStrings(Keys.federation.sets)) {
356                 logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
357                         gitblit.getFederationToken(set)));
358             }
359         }
360
361         // Schedule or run the federation executor
362         List<FederationModel> registrations = gitblit.getFederationRegistrations();
363         if (registrations.size() > 0) {
364             FederationPuller executor = new FederationPuller(registrations);
365             scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
366         }
367     }
368
67ba8f 369     @Override
JM 370     public boolean acceptsPush(Transport byTransport) {
7d3a31 371         if (byTransport == null) {
JM 372             logger.info("Unknown transport, push rejected!");
373             return false;
374         }
375
376         Set<Transport> transports = new HashSet<Transport>();
377         for (String value : settings.getStrings(Keys.git.acceptedPushTransports)) {
378             Transport transport = Transport.fromString(value);
379             if (transport == null) {
380                 logger.info(String.format("Ignoring unknown registered transport %s", value));
381                 continue;
382             }
383
384             transports.add(transport);
385         }
386
387         if (transports.isEmpty()) {
388             // no transports are explicitly specified, all are acceptable
389             return true;
390         }
391
392         // verify that the transport is permitted
393         return transports.contains(byTransport);
394     }
395
269c50 396     protected void configureGitDaemon() {
JM 397         int port = settings.getInteger(Keys.git.daemonPort, 0);
398         String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
399         if (port > 0) {
400             try {
401                 gitDaemon = new GitDaemon(gitblit);
402                 gitDaemon.start();
403             } catch (IOException e) {
404                 gitDaemon = null;
405                 logger.error(MessageFormat.format("Failed to start Git Daemon on {0}:{1,number,0}", bindInterface, port), e);
406             }
407         } else {
408             logger.info("Git Daemon is disabled.");
409         }
410     }
411
41124c 412     protected void configureSshDaemon() {
EM 413         int port = settings.getInteger(Keys.git.sshPort, 0);
414         String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost");
415         if (port > 0) {
416             try {
7d3a31 417                 sshDaemon = new SshDaemon(gitblit, workQueueProvider.get());
41124c 418                 sshDaemon.start();
EM 419             } catch (IOException e) {
420                 sshDaemon = null;
421                 logger.error(MessageFormat.format("Failed to start SSH daemon on {0}:{1,number,0}", bindInterface, port), e);
422             }
423         }
424     }
425
269c50 426     protected void configureFanout() {
JM 427         // startup Fanout PubSub service
428         if (settings.getInteger(Keys.fanout.port, 0) > 0) {
429             String bindInterface = settings.getString(Keys.fanout.bindInterface, null);
430             int port = settings.getInteger(Keys.fanout.port, FanoutService.DEFAULT_PORT);
431             boolean useNio = settings.getBoolean(Keys.fanout.useNio, true);
432             int limit = settings.getInteger(Keys.fanout.connectionLimit, 0);
433
434             if (useNio) {
435                 if (StringUtils.isEmpty(bindInterface)) {
436                     fanoutService = new FanoutNioService(port);
437                 } else {
438                     fanoutService = new FanoutNioService(bindInterface, port);
439                 }
440             } else {
441                 if (StringUtils.isEmpty(bindInterface)) {
442                     fanoutService = new FanoutSocketService(port);
443                 } else {
444                     fanoutService = new FanoutSocketService(bindInterface, port);
445                 }
446             }
447
448             fanoutService.setConcurrentConnectionLimit(limit);
449             fanoutService.setAllowAllChannelAnnouncements(false);
450             fanoutService.start();
451         } else {
452             logger.info("Fanout PubSub service is disabled.");
453         }
454     }
455
3a9e76 456     public String getGitDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) {
JM 457         if (gitDaemon != null) {
458             String bindInterface = settings.getString(Keys.git.daemonBindInterface, "localhost");
459             if (bindInterface.equals("localhost")
460                     && (!request.getServerName().equals("localhost") && !request.getServerName().equals("127.0.0.1"))) {
461                 // git daemon is bound to localhost and the request is from elsewhere
462                 return null;
463             }
464             if (user.canClone(repository)) {
58baee 465                 String hostname = getHostname(request);
JM 466                 String url = gitDaemon.formatUrl(hostname, repository.name);
3a9e76 467                 return url;
JM 468             }
469         }
470         return null;
471     }
472
473     public AccessPermission getGitDaemonAccessPermission(UserModel user, RepositoryModel repository) {
474         if (gitDaemon != null && user.canClone(repository)) {
475             AccessPermission gitDaemonPermission = user.getRepositoryPermission(repository).permission;
476             if (gitDaemonPermission.atLeast(AccessPermission.CLONE)) {
477                 if (repository.accessRestriction.atLeast(AccessRestrictionType.CLONE)) {
478                     // can not authenticate clone via anonymous git protocol
479                     gitDaemonPermission = AccessPermission.NONE;
480                 } else if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH)) {
481                     // can not authenticate push via anonymous git protocol
482                     gitDaemonPermission = AccessPermission.CLONE;
483                 } else {
484                     // normal user permission
485                 }
486             }
487             return gitDaemonPermission;
488         }
489         return AccessPermission.NONE;
490     }
491
58baee 492     public String getSshDaemonUrl(HttpServletRequest request, UserModel user, RepositoryModel repository) {
13331a 493         if (user == null || UserModel.ANONYMOUS.equals(user)) {
JM 494             // SSH always requires authentication - anonymous access prohibited
495             return null;
496         }
58baee 497         if (sshDaemon != null) {
JM 498             String bindInterface = settings.getString(Keys.git.sshBindInterface, "localhost");
499             if (bindInterface.equals("localhost")
500                     && (!request.getServerName().equals("localhost") && !request.getServerName().equals("127.0.0.1"))) {
501                 // ssh daemon is bound to localhost and the request is from elsewhere
502                 return null;
503             }
504             if (user.canClone(repository)) {
505                 String hostname = getHostname(request);
506                 String url = sshDaemon.formatUrl(user.username, hostname, repository.name);
507                 return url;
508             }
509         }
510         return null;
511     }
512
d5603a 513
58baee 514     /**
JM 515      * Extract the hostname from the canonical url or return the
516      * hostname from the servlet request.
d5603a 517      *
58baee 518      * @param request
JM 519      * @return
520      */
521     protected String getHostname(HttpServletRequest request) {
522         String hostname = request.getServerName();
7d3a31 523         String canonicalUrl = settings.getString(Keys.web.canonicalUrl, null);
58baee 524         if (!StringUtils.isEmpty(canonicalUrl)) {
JM 525             try {
526                 URI uri = new URI(canonicalUrl);
527                 String host = uri.getHost();
528                 if (!StringUtils.isEmpty(host) && !"localhost".equals(host)) {
529                     hostname = host;
530                 }
531             } catch (Exception e) {
532             }
533         }
534         return hostname;
535     }
3a9e76 536
7bf6e1 537     private class FederationPuller extends FederationPullService {
269c50 538
JM 539         public FederationPuller(FederationModel registration) {
23e08c 540             super(gitblit, Arrays.asList(registration));
269c50 541         }
JM 542
543         public FederationPuller(List<FederationModel> registrations) {
23e08c 544             super(gitblit, registrations);
269c50 545         }
JM 546
547         @Override
548         public void reschedule(FederationModel registration) {
549             // schedule the next pull
936af6 550             int mins = TimeUtils.convertFrequencyToMinutes(registration.frequency, 5);
269c50 551             registration.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
JM 552             scheduledExecutor.schedule(new FederationPuller(registration), mins, TimeUnit.MINUTES);
553             logger.info(MessageFormat.format(
554                     "Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
555                     registration.name, registration.url, registration.nextPull));
556         }
557     }
558 }