You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ServicesManager.java 17KB

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