View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.transport;
45  
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.OutputStream;
49  import java.net.InetAddress;
50  import java.net.InetSocketAddress;
51  import java.net.ServerSocket;
52  import java.net.Socket;
53  import java.net.SocketAddress;
54  import java.net.SocketException;
55  import java.util.concurrent.atomic.AtomicBoolean;
56  
57  import org.eclipse.jgit.errors.RepositoryNotFoundException;
58  import org.eclipse.jgit.internal.JGitText;
59  import org.eclipse.jgit.lib.PersonIdent;
60  import org.eclipse.jgit.lib.Repository;
61  import org.eclipse.jgit.storage.pack.PackConfig;
62  import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
63  import org.eclipse.jgit.transport.resolver.RepositoryResolver;
64  import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
65  import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
66  import org.eclipse.jgit.transport.resolver.UploadPackFactory;
67  
68  /** Basic daemon for the anonymous <code>git://</code> transport protocol. */
69  public class Daemon {
70  	/** 9418: IANA assigned port number for Git. */
71  	public static final int DEFAULT_PORT = 9418;
72  
73  	private static final int BACKLOG = 5;
74  
75  	private InetSocketAddress myAddress;
76  
77  	private final DaemonService[] services;
78  
79  	private final ThreadGroup processors;
80  
81  	private Acceptor acceptThread;
82  
83  	private int timeout;
84  
85  	private PackConfig packConfig;
86  
87  	private volatile RepositoryResolver<DaemonClient> repositoryResolver;
88  
89  	volatile UploadPackFactory<DaemonClient> uploadPackFactory;
90  
91  	volatile ReceivePackFactory<DaemonClient> receivePackFactory;
92  
93  	/** Configure a daemon to listen on any available network port. */
94  	public Daemon() {
95  		this(null);
96  	}
97  
98  	/**
99  	 * Configure a new daemon for the specified network address.
100 	 *
101 	 * @param addr
102 	 *            address to listen for connections on. If null, any available
103 	 *            port will be chosen on all network interfaces.
104 	 */
105 	@SuppressWarnings("unchecked")
106 	public Daemon(final InetSocketAddress addr) {
107 		myAddress = addr;
108 		processors = new ThreadGroup("Git-Daemon"); //$NON-NLS-1$
109 
110 		repositoryResolver = (RepositoryResolver<DaemonClient>) RepositoryResolver.NONE;
111 
112 		uploadPackFactory = new UploadPackFactory<DaemonClient>() {
113 			@Override
114 			public UploadPack create(DaemonClient req, Repository db)
115 					throws ServiceNotEnabledException,
116 					ServiceNotAuthorizedException {
117 				UploadPack up = new UploadPack(db);
118 				up.setTimeout(getTimeout());
119 				up.setPackConfig(getPackConfig());
120 				return up;
121 			}
122 		};
123 
124 		receivePackFactory = new ReceivePackFactory<DaemonClient>() {
125 			@Override
126 			public ReceivePack create(DaemonClient req, Repository db)
127 					throws ServiceNotEnabledException,
128 					ServiceNotAuthorizedException {
129 				ReceivePack rp = new ReceivePack(db);
130 
131 				InetAddress peer = req.getRemoteAddress();
132 				String host = peer.getCanonicalHostName();
133 				if (host == null)
134 					host = peer.getHostAddress();
135 				String name = "anonymous"; //$NON-NLS-1$
136 				String email = name + "@" + host; //$NON-NLS-1$
137 				rp.setRefLogIdent(new PersonIdent(name, email));
138 				rp.setTimeout(getTimeout());
139 
140 				return rp;
141 			}
142 		};
143 
144 		services = new DaemonService[] {
145 				new DaemonService("upload-pack", "uploadpack") { //$NON-NLS-1$ //$NON-NLS-2$
146 					{
147 						setEnabled(true);
148 					}
149 
150 					@Override
151 					protected void execute(final DaemonClient dc,
152 							final Repository db) throws IOException,
153 							ServiceNotEnabledException,
154 							ServiceNotAuthorizedException {
155 						UploadPack up = uploadPackFactory.create(dc, db);
156 						InputStream in = dc.getInputStream();
157 						OutputStream out = dc.getOutputStream();
158 						up.upload(in, out, null);
159 					}
160 				}, new DaemonService("receive-pack", "receivepack") { //$NON-NLS-1$ //$NON-NLS-2$
161 					{
162 						setEnabled(false);
163 					}
164 
165 					@Override
166 					protected void execute(final DaemonClient dc,
167 							final Repository db) throws IOException,
168 							ServiceNotEnabledException,
169 							ServiceNotAuthorizedException {
170 						ReceivePack rp = receivePackFactory.create(dc, db);
171 						InputStream in = dc.getInputStream();
172 						OutputStream out = dc.getOutputStream();
173 						rp.receive(in, out, null);
174 					}
175 				} };
176 	}
177 
178 	/** @return the address connections are received on. */
179 	public synchronized InetSocketAddress getAddress() {
180 		return myAddress;
181 	}
182 
183 	/**
184 	 * Lookup a supported service so it can be reconfigured.
185 	 *
186 	 * @param name
187 	 *            name of the service; e.g. "receive-pack"/"git-receive-pack" or
188 	 *            "upload-pack"/"git-upload-pack".
189 	 * @return the service; null if this daemon implementation doesn't support
190 	 *         the requested service type.
191 	 */
192 	public synchronized DaemonService getService(String name) {
193 		if (!name.startsWith("git-")) //$NON-NLS-1$
194 			name = "git-" + name; //$NON-NLS-1$
195 		for (final DaemonService s : services) {
196 			if (s.getCommandName().equals(name))
197 				return s;
198 		}
199 		return null;
200 	}
201 
202 	/** @return timeout (in seconds) before aborting an IO operation. */
203 	public int getTimeout() {
204 		return timeout;
205 	}
206 
207 	/**
208 	 * Set the timeout before willing to abort an IO call.
209 	 *
210 	 * @param seconds
211 	 *            number of seconds to wait (with no data transfer occurring)
212 	 *            before aborting an IO read or write operation with the
213 	 *            connected client.
214 	 */
215 	public void setTimeout(final int seconds) {
216 		timeout = seconds;
217 	}
218 
219 	/** @return configuration controlling packing, may be null. */
220 	public PackConfig getPackConfig() {
221 		return packConfig;
222 	}
223 
224 	/**
225 	 * Set the configuration used by the pack generator.
226 	 *
227 	 * @param pc
228 	 *            configuration controlling packing parameters. If null the
229 	 *            source repository's settings will be used.
230 	 */
231 	public void setPackConfig(PackConfig pc) {
232 		this.packConfig = pc;
233 	}
234 
235 	/**
236 	 * Set the resolver used to locate a repository by name.
237 	 *
238 	 * @param resolver
239 	 *            the resolver instance.
240 	 */
241 	public void setRepositoryResolver(RepositoryResolver<DaemonClient> resolver) {
242 		repositoryResolver = resolver;
243 	}
244 
245 	/**
246 	 * Set the factory to construct and configure per-request UploadPack.
247 	 *
248 	 * @param factory
249 	 *            the factory. If null upload-pack is disabled.
250 	 */
251 	@SuppressWarnings("unchecked")
252 	public void setUploadPackFactory(UploadPackFactory<DaemonClient> factory) {
253 		if (factory != null)
254 			uploadPackFactory = factory;
255 		else
256 			uploadPackFactory = (UploadPackFactory<DaemonClient>) UploadPackFactory.DISABLED;
257 	}
258 
259 	/**
260 	 * Get the factory used to construct per-request ReceivePack.
261 	 *
262 	 * @return the factory.
263 	 * @since 4.3
264 	 */
265 	public ReceivePackFactory<DaemonClient> getReceivePackFactory() {
266 		return receivePackFactory;
267 	}
268 
269 	/**
270 	 * Set the factory to construct and configure per-request ReceivePack.
271 	 *
272 	 * @param factory
273 	 *            the factory. If null receive-pack is disabled.
274 	 */
275 	@SuppressWarnings("unchecked")
276 	public void setReceivePackFactory(ReceivePackFactory<DaemonClient> factory) {
277 		if (factory != null)
278 			receivePackFactory = factory;
279 		else
280 			receivePackFactory = (ReceivePackFactory<DaemonClient>) ReceivePackFactory.DISABLED;
281 	}
282 
283 	private class Acceptor extends Thread {
284 
285 		private final ServerSocket listenSocket;
286 
287 		private final AtomicBoolean running = new AtomicBoolean(true);
288 
289 		public Acceptor(ThreadGroup group, String name, ServerSocket socket) {
290 			super(group, name);
291 			this.listenSocket = socket;
292 		}
293 
294 		@Override
295 		public void run() {
296 			setUncaughtExceptionHandler((thread, throwable) -> terminate());
297 			while (isRunning()) {
298 				try {
299 					startClient(listenSocket.accept());
300 				} catch (SocketException e) {
301 					// Test again to see if we should keep accepting.
302 				} catch (IOException e) {
303 					break;
304 				}
305 			}
306 
307 			terminate();
308 		}
309 
310 		private void terminate() {
311 			try {
312 				shutDown();
313 			} finally {
314 				clearThread();
315 			}
316 		}
317 
318 		public boolean isRunning() {
319 			return running.get();
320 		}
321 
322 		public void shutDown() {
323 			running.set(false);
324 			try {
325 				listenSocket.close();
326 			} catch (IOException err) {
327 				//
328 			}
329 		}
330 
331 	}
332 
333 	/**
334 	 * Start this daemon on a background thread.
335 	 *
336 	 * @throws IOException
337 	 *             the server socket could not be opened.
338 	 * @throws IllegalStateException
339 	 *             the daemon is already running.
340 	 */
341 	public synchronized void start() throws IOException {
342 		if (acceptThread != null) {
343 			throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
344 		}
345 		ServerSocket socket = new ServerSocket();
346 		socket.setReuseAddress(true);
347 		if (myAddress != null) {
348 			socket.bind(myAddress, BACKLOG);
349 		} else {
350 			socket.bind(new InetSocketAddress((InetAddress) null, 0), BACKLOG);
351 		}
352 		myAddress = (InetSocketAddress) socket.getLocalSocketAddress();
353 
354 		acceptThread = new Acceptor(processors, "Git-Daemon-Accept", socket); //$NON-NLS-1$
355 		acceptThread.start();
356 	}
357 
358 	private synchronized void clearThread() {
359 		acceptThread = null;
360 	}
361 
362 	/** @return true if this daemon is receiving connections. */
363 	public synchronized boolean isRunning() {
364 		return acceptThread != null && acceptThread.isRunning();
365 	}
366 
367 	/**
368 	 * Stop this daemon.
369 	 */
370 	public synchronized void stop() {
371 		if (acceptThread != null) {
372 			acceptThread.shutDown();
373 		}
374 	}
375 
376 	/**
377 	 * Stops this daemon and waits until it's acceptor thread has finished.
378 	 *
379 	 * @throws InterruptedException
380 	 *             if waiting for the acceptor thread is interrupted
381 	 *
382 	 * @since 4.9
383 	 */
384 	public void stopAndWait() throws InterruptedException {
385 		Thread acceptor = null;
386 		synchronized (this) {
387 			acceptor = acceptThread;
388 			stop();
389 		}
390 		if (acceptor != null) {
391 			acceptor.join();
392 		}
393 	}
394 
395 	void startClient(final Socket s) {
396 		final DaemonClient dc = new DaemonClient(this);
397 
398 		final SocketAddress peer = s.getRemoteSocketAddress();
399 		if (peer instanceof InetSocketAddress)
400 			dc.setRemoteAddress(((InetSocketAddress) peer).getAddress());
401 
402 		new Thread(processors, "Git-Daemon-Client " + peer.toString()) { //$NON-NLS-1$
403 			@Override
404 			public void run() {
405 				try {
406 					dc.execute(s);
407 				} catch (ServiceNotEnabledException e) {
408 					// Ignored. Client cannot use this repository.
409 				} catch (ServiceNotAuthorizedException e) {
410 					// Ignored. Client cannot use this repository.
411 				} catch (IOException e) {
412 					// Ignore unexpected IO exceptions from clients
413 				} finally {
414 					try {
415 						s.getInputStream().close();
416 					} catch (IOException e) {
417 						// Ignore close exceptions
418 					}
419 					try {
420 						s.getOutputStream().close();
421 					} catch (IOException e) {
422 						// Ignore close exceptions
423 					}
424 				}
425 			}
426 		}.start();
427 	}
428 
429 	synchronized DaemonService matchService(final String cmd) {
430 		for (final DaemonService d : services) {
431 			if (d.handles(cmd))
432 				return d;
433 		}
434 		return null;
435 	}
436 
437 	Repository openRepository(DaemonClient client, String name)
438 			throws ServiceMayNotContinueException {
439 		// Assume any attempt to use \ was by a Windows client
440 		// and correct to the more typical / used in Git URIs.
441 		//
442 		name = name.replace('\\', '/');
443 
444 		// git://thishost/path should always be name="/path" here
445 		//
446 		if (!name.startsWith("/")) //$NON-NLS-1$
447 			return null;
448 
449 		try {
450 			return repositoryResolver.open(client, name.substring(1));
451 		} catch (RepositoryNotFoundException e) {
452 			// null signals it "wasn't found", which is all that is suitable
453 			// for the remote client to know.
454 			return null;
455 		} catch (ServiceNotAuthorizedException e) {
456 			// null signals it "wasn't found", which is all that is suitable
457 			// for the remote client to know.
458 			return null;
459 		} catch (ServiceNotEnabledException e) {
460 			// null signals it "wasn't found", which is all that is suitable
461 			// for the remote client to know.
462 			return null;
463 		}
464 	}
465 }