From 40aa84507640cd2a980757e7910a63310474eb87 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 03 May 2013 19:09:25 -0400
Subject: [PATCH] Revised Git Daemon to improve thread stopping and to eliminate repository name hack

---
 src/main/java/com/gitblit/git/GitDaemon.java                 |  332 +++++++++++++++++++++++++--
 src/main/java/com/gitblit/git/GitblitReceivePackFactory.java |   13 
 src/main/java/com/gitblit/git/GitDaemonService.java          |  165 +++++++++++++
 releases.moxie                                               |    2 
 src/main/java/com/gitblit/git/GitDaemonClient.java           |  130 ++++++++++
 src/main/java/com/gitblit/GitBlit.java                       |    1 
 src/test/java/com/gitblit/tests/GitDaemonStopTest.java       |   33 ++
 src/main/java/com/gitblit/git/GitblitUploadPackFactory.java  |    5 
 src/main/java/com/gitblit/git/RepositoryResolver.java        |   18 +
 9 files changed, 650 insertions(+), 49 deletions(-)

diff --git a/releases.moxie b/releases.moxie
index c065435..ce71772 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -34,7 +34,7 @@
 	 
     additions: 
 	 - Added a server setting to force a particular translation/Locale for all sessions
-	 - Added Git Daemon serving
+	 - Added smart Git Daemon serving.  If enabled, git:// access will be offered for any repository which permits anonymous access.  If the repository permits anonymous cloning, anonymous git:// clone will be permitted while anonmymous git:// pushes will be rejected.
 	 - Option to automatically tag branch tips on each push with an incremental revision number
      - Implemented multiple repository owners
      - Optional periodic LDAP user and team pre-fetching & synchronization
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index 377a7b3..0e4e2e9 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -3240,7 +3240,6 @@
 			try {
 				gitDaemon = new GitDaemon(bindInterface, port, getRepositoriesFolder());
 				gitDaemon.start();
-				logger.info(MessageFormat.format("Git daemon is listening on {0}:{1,number,0}", bindInterface, port));
 			} catch (IOException e) {
 				gitDaemon = null;
 				logger.error(MessageFormat.format("Failed to start Git daemon on {0}:{1,number,0}", bindInterface, port), e);
diff --git a/src/main/java/com/gitblit/git/GitDaemon.java b/src/main/java/com/gitblit/git/GitDaemon.java
index 8ec0563..7050f87 100644
--- a/src/main/java/com/gitblit/git/GitDaemon.java
+++ b/src/main/java/com/gitblit/git/GitDaemon.java
@@ -1,36 +1,115 @@
 /*
- * Copyright 2013 gitblit.com.
+ * Copyright (C) 2013 gitblit.com
+ * Copyright (C) 2008-2009, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
  *
- * 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
+ * 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
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ * All rights reserved.
  *
- * 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.
+ * 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.transport.Daemon;
-import org.eclipse.jgit.transport.DaemonClient;
+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.
+ * 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 extends Daemon {
+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<GitDaemonClient> repositoryResolver;
+
+	private UploadPackFactory<GitDaemonClient> uploadPackFactory;
+
+	private ReceivePackFactory<GitDaemonClient> receivePackFactory;
+
+	/** Configure a daemon to listen on any available network port. */
+	public GitDaemon() {
+		this(null);
+	}
 
 	/**
 	 * Construct the Gitblit Git daemon.
@@ -43,22 +122,213 @@
 	 *            the folder to serve from
 	 */
 	public GitDaemon(String bindInterface, int port, File folder) {
-		super(StringUtils.isEmpty(bindInterface) ? new InetSocketAddress(port) : new InetSocketAddress(bindInterface, port));
-		
+		this(StringUtils.isEmpty(bindInterface) ? new InetSocketAddress(port)
+				: new InetSocketAddress(bindInterface, port));
+
 		// set the repository resolver and pack factories
-		setRepositoryResolver(new RepositoryResolver<DaemonClient>(folder));
-		setUploadPackFactory(new GitblitUploadPackFactory<DaemonClient>());
-		setReceivePackFactory(new GitblitReceivePackFactory<DaemonClient>());
-		
-		// configure the git daemon to ignore the per-repository settings,
-		// daemon.uploadpack and daemon.receivepack
-		getService("git-upload-pack").setOverridable(false);
-		getService("git-receive-pack").setOverridable(false);
-		
-		// enable both the upload and receive services and let the resolver,
-		// pack factories, and receive hook handle security
-		getService("git-upload-pack").setEnabled(true);
-		getService("git-receive-pack").setEnabled(true);
+		repositoryResolver = new RepositoryResolver<GitDaemonClient>(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<GitDaemonClient>();
+		receivePackFactory = new GitblitReceivePackFactory<GitDaemonClient>();
+
+		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);
+					}
+				} };
+	}
+
+	/** @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 = 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 (acceptThread != null) {
+			logger.info("Git Daemon stopping...");
+			run.set(false);
+			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);
+			}
+		}
+	}
+
+	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;
+		}
+	}
 }
diff --git a/src/main/java/com/gitblit/git/GitDaemonClient.java b/src/main/java/com/gitblit/git/GitDaemonClient.java
new file mode 100644
index 0000000..6972f31
--- /dev/null
+++ b/src/main/java/com/gitblit/git/GitDaemonClient.java
@@ -0,0 +1,130 @@
+package com.gitblit.git;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+
+import org.eclipse.jgit.transport.PacketLineIn;
+import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.util.io.SafeBufferedOutputStream;
+
+/** Active network client of {@link Daemon}. */
+public class GitDaemonClient {
+	private final GitDaemon daemon;
+
+	private InetAddress peer;
+
+	private InputStream rawIn;
+
+	private OutputStream rawOut;
+	
+	private String repositoryName;
+
+	GitDaemonClient(final GitDaemon d) {
+		daemon = d;
+	}
+
+	void setRemoteAddress(final InetAddress ia) {
+		peer = ia;
+	}
+
+	/** @return the daemon which spawned this client. */
+	public GitDaemon getDaemon() {
+		return daemon;
+	}
+
+	/** @return Internet address of the remote client. */
+	public InetAddress getRemoteAddress() {
+		return peer;
+	}
+
+	/** @return input stream to read from the connected client. */
+	public InputStream getInputStream() {
+		return rawIn;
+	}
+
+	/** @return output stream to send data to the connected client. */
+	public OutputStream getOutputStream() {
+		return rawOut;
+	}
+	
+	public void setRepositoryName(String repositoryName) {
+		this.repositoryName = repositoryName;
+	}
+	
+	/** @return the name of the requested repository. */
+	public String getRepositoryName() {
+		return repositoryName;
+	}
+
+	void execute(final Socket sock) throws IOException,
+			ServiceNotEnabledException, ServiceNotAuthorizedException {
+		rawIn = new BufferedInputStream(sock.getInputStream());
+		rawOut = new SafeBufferedOutputStream(sock.getOutputStream());
+
+		if (0 < daemon.getTimeout())
+			sock.setSoTimeout(daemon.getTimeout() * 1000);
+		String cmd = new PacketLineIn(rawIn).readStringRaw();
+		final int nul = cmd.indexOf('\0');
+		if (nul >= 0) {
+			// Newer clients hide a "host" header behind this byte.
+			// Currently we don't use it for anything, so we ignore
+			// this portion of the command.
+			//
+			cmd = cmd.substring(0, nul);
+		}
+
+		final GitDaemonService srv = getDaemon().matchService(cmd);
+		if (srv == null)
+			return;
+		sock.setSoTimeout(0);
+		srv.execute(this, cmd);
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/git/GitDaemonService.java b/src/main/java/com/gitblit/git/GitDaemonService.java
new file mode 100644
index 0000000..03c4e1c
--- /dev/null
+++ b/src/main/java/com/gitblit/git/GitDaemonService.java
@@ -0,0 +1,165 @@
+package com.gitblit.git;
+
+/*
+ * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2009, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * 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.
+ */
+
+import java.io.IOException;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.Daemon;
+import org.eclipse.jgit.transport.PacketLineOut;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
+
+/** A service exposed by {@link Daemon} over anonymous <code>git://</code>. */
+public abstract class GitDaemonService {
+	private final String command;
+
+	private final SectionParser<ServiceConfig> configKey;
+
+	private boolean enabled;
+
+	private boolean overridable;
+
+	GitDaemonService(final String cmdName, final String cfgName) {
+		command = cmdName.startsWith("git-") ? cmdName : "git-" + cmdName; //$NON-NLS-1$ //$NON-NLS-2$
+		configKey = new SectionParser<ServiceConfig>() {
+			public ServiceConfig parse(final Config cfg) {
+				return new ServiceConfig(GitDaemonService.this, cfg, cfgName);
+			}
+		};
+		overridable = true;
+	}
+
+	private static class ServiceConfig {
+		final boolean enabled;
+
+		ServiceConfig(final GitDaemonService service, final Config cfg,
+				final String name) {
+			enabled = cfg.getBoolean("daemon", name, service.isEnabled()); //$NON-NLS-1$
+		}
+	}
+
+	/** @return is this service enabled for invocation? */
+	public boolean isEnabled() {
+		return enabled;
+	}
+
+	/**
+	 * @param on
+	 *            true to allow this service to be used; false to deny it.
+	 */
+	public void setEnabled(final boolean on) {
+		enabled = on;
+	}
+
+	/** @return can this service be configured in the repository config file? */
+	public boolean isOverridable() {
+		return overridable;
+	}
+
+	/**
+	 * @param on
+	 *            true to permit repositories to override this service's enabled
+	 *            state with the <code>daemon.servicename</code> config setting.
+	 */
+	public void setOverridable(final boolean on) {
+		overridable = on;
+	}
+
+	/** @return name of the command requested by clients. */
+	public String getCommandName() {
+		return command;
+	}
+
+	/**
+	 * Determine if this service can handle the requested command.
+	 *
+	 * @param commandLine
+	 *            input line from the client.
+	 * @return true if this command can accept the given command line.
+	 */
+	public boolean handles(final String commandLine) {
+		return command.length() + 1 < commandLine.length()
+				&& commandLine.charAt(command.length()) == ' '
+				&& commandLine.startsWith(command);
+	}
+
+	void execute(final GitDaemonClient client, final String commandLine)
+			throws IOException, ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		final String name = commandLine.substring(command.length() + 1);
+		Repository db;
+		try {
+			db = client.getDaemon().openRepository(client, name);
+		} catch (ServiceMayNotContinueException e) {
+			// An error when opening the repo means the client is expecting a ref
+			// advertisement, so use that style of error.
+			PacketLineOut pktOut = new PacketLineOut(client.getOutputStream());
+			pktOut.writeString("ERR " + e.getMessage() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
+			db = null;
+		}
+		if (db == null)
+			return;
+		try {
+			if (isEnabledFor(db))
+				execute(client, db);
+		} finally {
+			db.close();
+		}
+	}
+
+	private boolean isEnabledFor(final Repository db) {
+		if (isOverridable())
+			return db.getConfig().get(configKey).enabled;
+		return isEnabled();
+	}
+
+	abstract void execute(GitDaemonClient client, Repository db)
+			throws IOException, ServiceNotEnabledException,
+			ServiceNotAuthorizedException;
+}
diff --git a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
index 8b8ef0e..77a3df6 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
+++ b/src/main/java/com/gitblit/git/GitblitReceivePackFactory.java
@@ -19,7 +19,6 @@
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.DaemonClient;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
@@ -50,18 +49,15 @@
 
 		final ReceivePack rp = new ReceivePack(db);
 		UserModel user = UserModel.ANONYMOUS;
+		String repositoryName = "";
 		String origin = "";
 		String gitblitUrl = "";
 		int timeout = 0;
 		
-		// XXX extract the repository name from the config
-		// the name is injected by GitRepositoryResolver
-		String repositoryName = db.getConfig().getString("gitblit", null, "repositoryName");
-		
-		
 		if (req instanceof HttpServletRequest) {
 			// http/https request may or may not be authenticated 
 			HttpServletRequest request = (HttpServletRequest) req;
+			repositoryName = request.getAttribute("gitblitRepositoryName").toString();
 			origin = request.getRemoteHost();
 			gitblitUrl = HttpUtils.getGitblitURL(request);
 
@@ -74,9 +70,10 @@
 					user = new UserModel(username);
 				}
 			}
-		} else if (req instanceof DaemonClient) {
+		} else if (req instanceof GitDaemonClient) {
 			// git daemon request is alway anonymous
-			DaemonClient client = (DaemonClient) req;
+			GitDaemonClient client = (GitDaemonClient) req;
+			repositoryName = client.getRepositoryName();
 			origin = client.getRemoteAddress().getHostAddress();
 			// set timeout from Git daemon
 			timeout = client.getDaemon().getTimeout();
diff --git a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java
index 85750f8..e953ca4 100644
--- a/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java
+++ b/src/main/java/com/gitblit/git/GitblitUploadPackFactory.java
@@ -21,7 +21,6 @@
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.DaemonClient;
 import org.eclipse.jgit.transport.RefFilter;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
@@ -56,9 +55,9 @@
 			if (user == null) {
 				user = UserModel.ANONYMOUS;
 			}
-		} else if (req instanceof DaemonClient) {
+		} else if (req instanceof GitDaemonClient) {
 			// git daemon request is always anonymous
-			DaemonClient client = (DaemonClient) req;
+			GitDaemonClient client = (GitDaemonClient) req;
 			// set timeout from Git daemon
 			timeout = client.getDaemon().getTimeout();
 		}
diff --git a/src/main/java/com/gitblit/git/RepositoryResolver.java b/src/main/java/com/gitblit/git/RepositoryResolver.java
index fb5db71..21a8376 100644
--- a/src/main/java/com/gitblit/git/RepositoryResolver.java
+++ b/src/main/java/com/gitblit/git/RepositoryResolver.java
@@ -23,7 +23,6 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.DaemonClient;
 import org.eclipse.jgit.transport.resolver.FileResolver;
 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.slf4j.Logger;
@@ -54,10 +53,19 @@
 	public Repository open(final X req, final String name)
 			throws RepositoryNotFoundException, ServiceNotEnabledException {
 		Repository repo = super.open(req, name);
-		// XXX Set repository name for the pack factories
+		
+		// Set repository name for the pack factories
 		// We do this because the JGit API does not have a consistent way to
 		// retrieve the repository name from the pack factories or the hooks.
-		repo.getConfig().setString("gitblit", null, "repositoryName", name);
+		if (req instanceof HttpServletRequest) {
+			// http/https request
+			HttpServletRequest client = (HttpServletRequest) req;
+			client.setAttribute("gitblitRepositoryName", name);
+		} else if (req instanceof GitDaemonClient) {
+			// git request
+			GitDaemonClient client = (GitDaemonClient) req;
+			client.setRepositoryName(name);
+		}
 		return repo;
 	}
 	
@@ -72,10 +80,10 @@
 		UserModel user = null;
 		String origin = null;
 		
-		if (req instanceof DaemonClient) {
+		if (req instanceof GitDaemonClient) {
 			// git daemon request
 			// this is an anonymous/unauthenticated protocol
-			DaemonClient client = (DaemonClient) req;
+			GitDaemonClient client = (GitDaemonClient) req;
 			scheme = "git";
 			origin = client.getRemoteAddress().toString();
 			user = UserModel.ANONYMOUS;
diff --git a/src/test/java/com/gitblit/tests/GitDaemonStopTest.java b/src/test/java/com/gitblit/tests/GitDaemonStopTest.java
new file mode 100644
index 0000000..7febc7a
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/GitDaemonStopTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.tests;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.gitblit.git.GitDaemon;
+
+public class GitDaemonStopTest extends Assert {
+
+	@Test
+	public void testGitDaemonStop() throws Exception {
+		GitDaemon daemon = new GitDaemon("localhost", GitDaemon.DEFAULT_PORT + 1, GitBlitSuite.REPOSITORIES);
+		daemon.setTimeout(5);
+		daemon.start();
+		Thread.sleep(5000);
+		daemon.stop();
+	}
+}

--
Gitblit v1.9.1