/* * Copyright (C) 2013 gitblit.com * Copyright (C) 2008-2009, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.gitblit.git; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.text.MessageFormat; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.UploadPack; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; import org.eclipse.jgit.transport.resolver.UploadPackFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.utils.StringUtils; /** * Gitblit's Git Daemon ignores any and all per-repository daemon settings and * integrates into Gitblit's security model. * * @author James Moger * */ public class GitDaemon { private final Logger logger = LoggerFactory.getLogger(GitDaemon.class); /** 9418: IANA assigned port number for Git. */ public static final int DEFAULT_PORT = 9418; private static final int BACKLOG = 5; private InetSocketAddress myAddress; private final GitDaemonService[] services; private final ThreadGroup processors; private AtomicBoolean run; private ServerSocket acceptSocket; private Thread acceptThread; private int timeout; private RepositoryResolver repositoryResolver; private UploadPackFactory uploadPackFactory; private ReceivePackFactory receivePackFactory; /** Configure a daemon to listen on any available network port. */ public GitDaemon() { this(null); } /** * Construct the Gitblit Git daemon. * * @param bindInterface * the ip address of the interface to bind * @param port * the port to serve on * @param folder * the folder to serve from */ public GitDaemon(String bindInterface, int port, File folder) { this(StringUtils.isEmpty(bindInterface) ? new InetSocketAddress(port) : new InetSocketAddress(bindInterface, port)); // set the repository resolver and pack factories repositoryResolver = new RepositoryResolver(folder); } /** * Configure a new daemon for the specified network address. * * @param addr * address to listen for connections on. If null, any available * port will be chosen on all network interfaces. */ public GitDaemon(final InetSocketAddress addr) { myAddress = addr; processors = new ThreadGroup("Git-Daemon"); run = new AtomicBoolean(false); repositoryResolver = null; uploadPackFactory = new GitblitUploadPackFactory(); receivePackFactory = new GitblitReceivePackFactory(); services = new GitDaemonService[] { new GitDaemonService("upload-pack", "uploadpack") { { setEnabled(true); setOverridable(false); } @Override protected void execute(final GitDaemonClient dc, final Repository db) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { UploadPack up = uploadPackFactory.create(dc, db); InputStream in = dc.getInputStream(); OutputStream out = dc.getOutputStream(); up.upload(in, out, null); } }, new GitDaemonService("receive-pack", "receivepack") { { setEnabled(true); setOverridable(false); } @Override protected void execute(final GitDaemonClient dc, final Repository db) throws IOException, ServiceNotEnabledException, ServiceNotAuthorizedException { ReceivePack rp = receivePackFactory.create(dc, db); InputStream in = dc.getInputStream(); OutputStream out = dc.getOutputStream(); rp.receive(in, out, null); } } }; } public int getPort() { return myAddress.getPort(); } public String formatUrl(String servername, String repository) { if (getPort() == 9418) { // standard port return MessageFormat.format("git://{0}/{1}", servername, repository); } else { // non-standard port return MessageFormat.format("git://{0}:{1,number,0}/{2}", servername, getPort(), repository); } } /** @return timeout (in seconds) before aborting an IO operation. */ public int getTimeout() { return timeout; } /** * Set the timeout before willing to abort an IO call. * * @param seconds * number of seconds to wait (with no data transfer occurring) * before aborting an IO read or write operation with the * connected client. */ public void setTimeout(final int seconds) { timeout = seconds; } /** * Start this daemon on a background thread. * * @throws IOException * the server socket could not be opened. * @throws IllegalStateException * the daemon is already running. */ public synchronized void start() throws IOException { if (acceptThread != null) throw new IllegalStateException(JGitText.get().daemonAlreadyRunning); final ServerSocket listenSock = new ServerSocket(myAddress != null ? myAddress.getPort() : 0, BACKLOG, myAddress != null ? myAddress.getAddress() : null); myAddress = (InetSocketAddress) listenSock.getLocalSocketAddress(); run.set(true); acceptSocket = listenSock; acceptThread = new Thread(processors, "Git-Daemon-Accept") { public void run() { while (isRunning()) { try { startClient(listenSock.accept()); } catch (InterruptedIOException e) { // Test again to see if we should keep accepting. } catch (IOException e) { break; } } try { listenSock.close(); } catch (IOException err) { // } finally { acceptSocket = null; } } }; acceptThread.start(); logger.info(MessageFormat.format("Git Daemon is listening on {0}:{1,number,0}", myAddress.getAddress().getHostAddress(), myAddress.getPort())); } /** @return true if this daemon is receiving connections. */ public boolean isRunning() { return run.get(); } /** Stop this daemon. */ public synchronized void stop() { if (isRunning() && acceptThread != null) { run.set(false); logger.info("Git Daemon stopping..."); try { // close the accept socket // this throws a SocketException in the accept thread acceptSocket.close(); } catch (IOException e1) { } try { // join the accept thread acceptThread.join(); logger.info("Git Daemon stopped."); } catch (InterruptedException e) { logger.error("Accept thread join interrupted", e); } finally { acceptThread = null; } } } private void startClient(final Socket s) { final GitDaemonClient dc = new GitDaemonClient(this); final SocketAddress peer = s.getRemoteSocketAddress(); if (peer instanceof InetSocketAddress) dc.setRemoteAddress(((InetSocketAddress) peer).getAddress()); new Thread(processors, "Git-Daemon-Client " + peer.toString()) { public void run() { try { dc.execute(s); } catch (ServiceNotEnabledException e) { // Ignored. Client cannot use this repository. } catch (ServiceNotAuthorizedException e) { // Ignored. Client cannot use this repository. } catch (IOException e) { // Ignore unexpected IO exceptions from clients } finally { try { s.getInputStream().close(); } catch (IOException e) { // Ignore close exceptions } try { s.getOutputStream().close(); } catch (IOException e) { // Ignore close exceptions } } } }.start(); } synchronized GitDaemonService matchService(final String cmd) { for (final GitDaemonService d : services) { if (d.handles(cmd)) return d; } return null; } Repository openRepository(GitDaemonClient client, String name) throws ServiceMayNotContinueException { // Assume any attempt to use \ was by a Windows client // and correct to the more typical / used in Git URIs. // name = name.replace('\\', '/'); // git://thishost/path should always be name="/path" here // if (!name.startsWith("/")) //$NON-NLS-1$ return null; try { return repositoryResolver.open(client, name.substring(1)); } catch (RepositoryNotFoundException e) { // null signals it "wasn't found", which is all that is suitable // for the remote client to know. return null; } catch (ServiceNotEnabledException e) { // null signals it "wasn't found", which is all that is suitable // for the remote client to know. return null; } } } id='n223' href='#n223'>223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
/*
 * 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.servlet;

import java.io.File;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import javax.servlet.http.HttpServletResponse;

import com.gitblit.Constants.FederationRequest;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.models.FederationModel;
import com.gitblit.models.FederationProposal;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.FederationUtils;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;

/**
 * Handles federation requests.
 *
 * @author James Moger
 *
 */
@Singleton
public class FederationServlet extends JsonServlet {

	private static final long serialVersionUID = 1L;

	private IStoredSettings settings;

	private IUserManager userManager;

	private IRepositoryManager repositoryManager;

	private IFederationManager federationManager;

	@Inject
	public FederationServlet(
			IStoredSettings settings,
			IUserManager userManager,
			IRepositoryManager repositoryManager,
			IFederationManager federationManager) {

		this.settings = settings;
		this.userManager = userManager;
		this.repositoryManager = repositoryManager;
		this.federationManager = federationManager;
	}

	/**
	 * Processes a federation request.
	 *
	 * @param request
	 * @param response
	 * @throws javax.servlet.ServletException
	 * @throws java.io.IOException
	 */

	@Override
	protected void processRequest(javax.servlet.http.HttpServletRequest request,
			javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
			java.io.IOException {

		FederationRequest reqType = FederationRequest.fromName(request.getParameter("req"));
		logger.info(MessageFormat.format("Federation {0} request from {1}", reqType,
				request.getRemoteAddr()));

		if (FederationRequest.POKE.equals(reqType)) {
			// Gitblit always responds to POKE requests to verify a connection
			logger.info("Received federation POKE from " + request.getRemoteAddr());
			return;
		}

		if (!settings.getBoolean(Keys.git.enableGitServlet, true)) {
			logger.warn(Keys.git.enableGitServlet + " must be set TRUE for federation requests.");
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		}

		String uuid = settings.getString(Keys.federation.passphrase, "");
		if (StringUtils.isEmpty(uuid)) {
			logger.warn(Keys.federation.passphrase
					+ " is not properly set!  Federation request denied.");
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		}

		if (FederationRequest.PROPOSAL.equals(reqType)) {
			// Receive a gitblit federation proposal
			FederationProposal proposal = deserialize(request, response, FederationProposal.class);
			if (proposal == null) {
				return;
			}

			// reject proposal, if not receipt prohibited
			if (!settings.getBoolean(Keys.federation.allowProposals, false)) {
				logger.error(MessageFormat.format("Rejected {0} federation proposal from {1}",
						proposal.tokenType.name(), proposal.url));
				response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
				return;
			}

			// poke the origin Gitblit instance that is proposing federation
			boolean poked = false;
			try {
				poked = FederationUtils.poke(proposal.url);
			} catch (Exception e) {
				logger.error("Failed to poke origin", e);
			}
			if (!poked) {
				logger.error(MessageFormat.format("Failed to send federation poke to {0}",
						proposal.url));
				response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
				return;
			}

			String gitblitUrl = settings.getString(Keys.web.canonicalUrl, null);
			if (StringUtils.isEmpty(gitblitUrl)) {
				gitblitUrl = HttpUtils.getGitblitURL(request);
			}
			federationManager.submitFederationProposal(proposal, gitblitUrl);
			logger.info(MessageFormat.format(
					"Submitted {0} federation proposal to pull {1} repositories from {2}",
					proposal.tokenType.name(), proposal.repositories.size(), proposal.url));
			response.setStatus(HttpServletResponse.SC_OK);
			return;
		}

		if (FederationRequest.STATUS.equals(reqType)) {
			// Receive a gitblit federation status acknowledgment
			String remoteId = StringUtils.decodeFromHtml(request.getParameter("url"));
			String identification = MessageFormat.format("{0} ({1})", remoteId,
					request.getRemoteAddr());

			// deserialize the status data
			FederationModel results = deserialize(request, response, FederationModel.class);
			if (results == null) {
				return;
			}

			// setup the last and netx pull dates
			results.lastPull = new Date();
			int mins = TimeUtils.convertFrequencyToMinutes(results.frequency, 5);
			results.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));

			// acknowledge the receipt of status
			federationManager.acknowledgeFederationStatus(identification, results);
			logger.info(MessageFormat.format(
					"Received status of {0} federated repositories from {1}", results
							.getStatusList().size(), identification));
			response.setStatus(HttpServletResponse.SC_OK);
			return;
		}

		// Determine the federation tokens for this gitblit instance
		String token = request.getParameter("token");
		List<String> tokens = federationManager.getFederationTokens();
		if (!tokens.contains(token)) {
			logger.warn(MessageFormat.format(
					"Received Federation token ''{0}'' does not match the server tokens", token));
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		}

		Object result = null;
		if (FederationRequest.PULL_REPOSITORIES.equals(reqType)) {
			String gitblitUrl = settings.getString(Keys.web.canonicalUrl, null);
			if (StringUtils.isEmpty(gitblitUrl)) {
				gitblitUrl = HttpUtils.getGitblitURL(request);
			}
			result = federationManager.getRepositories(gitblitUrl, token);
		} else {
			if (FederationRequest.PULL_SETTINGS.equals(reqType)) {
				// pull settings
				if (!federationManager.validateFederationRequest(reqType, token)) {
					// invalid token to pull users or settings
					logger.warn(MessageFormat.format(
							"Federation token from {0} not authorized to pull SETTINGS",
							request.getRemoteAddr()));
					response.sendError(HttpServletResponse.SC_FORBIDDEN);
					return;
				}
				Map<String, String> map = new HashMap<String, String>();
				List<String> keys = settings.getAllKeys(null);
				for (String key : keys) {
					map.put(key, settings.getString(key, ""));
				}
				result = map;
			} else if (FederationRequest.PULL_USERS.equals(reqType)) {
				// pull users
				if (!federationManager.validateFederationRequest(reqType, token)) {
					// invalid token to pull users or settings
					logger.warn(MessageFormat.format(
							"Federation token from {0} not authorized to pull USERS",
							request.getRemoteAddr()));
					response.sendError(HttpServletResponse.SC_FORBIDDEN);
					return;
				}
				List<String> usernames = userManager.getAllUsernames();
				List<UserModel> users = new ArrayList<UserModel>();
				for (String username : usernames) {
					UserModel user = userManager.getUserModel(username);
					if (!user.excludeFromFederation) {
						users.add(user);
					}
				}
				result = users;
			} else if (FederationRequest.PULL_TEAMS.equals(reqType)) {
				// pull teams
				if (!federationManager.validateFederationRequest(reqType, token)) {
					// invalid token to pull teams
					logger.warn(MessageFormat.format(
							"Federation token from {0} not authorized to pull TEAMS",
							request.getRemoteAddr()));
					response.sendError(HttpServletResponse.SC_FORBIDDEN);
					return;
				}
				List<String> teamnames = userManager.getAllTeamNames();
				List<TeamModel> teams = new ArrayList<TeamModel>();
				for (String teamname : teamnames) {
					TeamModel user = userManager.getTeamModel(teamname);
					teams.add(user);
				}
				result = teams;
			} else if (FederationRequest.PULL_SCRIPTS.equals(reqType)) {
				// pull scripts
				if (!federationManager.validateFederationRequest(reqType, token)) {
					// invalid token to pull script
					logger.warn(MessageFormat.format(
							"Federation token from {0} not authorized to pull SCRIPTS",
							request.getRemoteAddr()));
					response.sendError(HttpServletResponse.SC_FORBIDDEN);
					return;
				}
				Map<String, String> scripts = new HashMap<String, String>();

				Set<String> names = new HashSet<String>();
				names.addAll(settings.getStrings(Keys.groovy.preReceiveScripts));
				names.addAll(settings.getStrings(Keys.groovy.postReceiveScripts));
				for (TeamModel team :  userManager.getAllTeams()) {
					names.addAll(team.preReceiveScripts);
					names.addAll(team.postReceiveScripts);
				}
				File scriptsFolder = repositoryManager.getHooksFolder();
				for (String name : names) {
					File file = new File(scriptsFolder, name);
					if (!file.exists() && !file.getName().endsWith(".groovy")) {
						file = new File(scriptsFolder, name + ".groovy");
					}
					if (file.exists()) {
						// read the script
						String content = FileUtils.readContent(file, "\n");
						scripts.put(name, content);
					} else {
						// missing script?!
						logger.warn(MessageFormat.format("Failed to find push script \"{0}\"", name));
					}
				}
				result = scripts;
			}
		}

		// send the result of the request
		serialize(response, result);
	}
}