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  import java.util.Collection;
57  
58  import org.eclipse.jgit.annotations.Nullable;
59  import org.eclipse.jgit.errors.RepositoryNotFoundException;
60  import org.eclipse.jgit.internal.JGitText;
61  import org.eclipse.jgit.lib.PersonIdent;
62  import org.eclipse.jgit.lib.Repository;
63  import org.eclipse.jgit.storage.pack.PackConfig;
64  import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
65  import org.eclipse.jgit.transport.resolver.RepositoryResolver;
66  import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
67  import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
68  import org.eclipse.jgit.transport.resolver.UploadPackFactory;
69  
70  /**
71   * Basic daemon for the anonymous <code>git://</code> transport protocol.
72   */
73  public class Daemon {
74  	/** 9418: IANA assigned port number for Git. */
75  	public static final int DEFAULT_PORT = 9418;
76  
77  	private static final int BACKLOG = 5;
78  
79  	private InetSocketAddress myAddress;
80  
81  	private final DaemonService[] services;
82  
83  	private final ThreadGroup processors;
84  
85  	private Acceptor acceptThread;
86  
87  	private int timeout;
88  
89  	private PackConfig packConfig;
90  
91  	private volatile RepositoryResolver<DaemonClient> repositoryResolver;
92  
93  	volatile UploadPackFactory<DaemonClient> uploadPackFactory;
94  
95  	volatile ReceivePackFactory<DaemonClient> receivePackFactory;
96  
97  	/**
98  	 * Configure a daemon to listen on any available network port.
99  	 */
100 	public Daemon() {
101 		this(null);
102 	}
103 
104 	/**
105 	 * Configure a new daemon for the specified network address.
106 	 *
107 	 * @param addr
108 	 *            address to listen for connections on. If null, any available
109 	 *            port will be chosen on all network interfaces.
110 	 */
111 	@SuppressWarnings("unchecked")
112 	public Daemon(InetSocketAddress addr) {
113 		myAddress = addr;
114 		processors = new ThreadGroup("Git-Daemon"); //$NON-NLS-1$
115 
116 		repositoryResolver = (RepositoryResolver<DaemonClient>) RepositoryResolver.NONE;
117 
118 		uploadPackFactory = new UploadPackFactory<DaemonClient>() {
119 			@Override
120 			public UploadPack create(DaemonClient req, Repository db)
121 					throws ServiceNotEnabledException,
122 					ServiceNotAuthorizedException {
123 				UploadPack up = new UploadPack(db);
124 				up.setTimeout(getTimeout());
125 				up.setPackConfig(getPackConfig());
126 				return up;
127 			}
128 		};
129 
130 		receivePackFactory = new ReceivePackFactory<DaemonClient>() {
131 			@Override
132 			public ReceivePack create(DaemonClient req, Repository db)
133 					throws ServiceNotEnabledException,
134 					ServiceNotAuthorizedException {
135 				ReceivePack rp = new ReceivePack(db);
136 
137 				InetAddress peer = req.getRemoteAddress();
138 				String host = peer.getCanonicalHostName();
139 				if (host == null)
140 					host = peer.getHostAddress();
141 				String name = "anonymous"; //$NON-NLS-1$
142 				String email = name + "@" + host; //$NON-NLS-1$
143 				rp.setRefLogIdent(new PersonIdent(name, email));
144 				rp.setTimeout(getTimeout());
145 
146 				return rp;
147 			}
148 		};
149 
150 		services = new DaemonService[] {
151 				new DaemonService("upload-pack", "uploadpack") { //$NON-NLS-1$ //$NON-NLS-2$
152 					{
153 						setEnabled(true);
154 					}
155 
156 					@Override
157 					protected void execute(final DaemonClient dc,
158 							final Repository db,
159 							@Nullable Collection<String> extraParameters)
160 							throws IOException,
161 							ServiceNotEnabledException,
162 							ServiceNotAuthorizedException {
163 						UploadPack up = uploadPackFactory.create(dc, db);
164 						InputStream in = dc.getInputStream();
165 						OutputStream out = dc.getOutputStream();
166 						if (extraParameters != null) {
167 							up.setExtraParameters(extraParameters);
168 						}
169 						up.upload(in, out, null);
170 					}
171 				}, new DaemonService("receive-pack", "receivepack") { //$NON-NLS-1$ //$NON-NLS-2$
172 					{
173 						setEnabled(false);
174 					}
175 
176 					@Override
177 					protected void execute(final DaemonClient dc,
178 							final Repository db,
179 							@Nullable Collection<String> extraParameters)
180 							throws IOException,
181 							ServiceNotEnabledException,
182 							ServiceNotAuthorizedException {
183 						ReceivePack rp = receivePackFactory.create(dc, db);
184 						InputStream in = dc.getInputStream();
185 						OutputStream out = dc.getOutputStream();
186 						rp.receive(in, out, null);
187 					}
188 				} };
189 	}
190 
191 	/**
192 	 * Get the address connections are received on.
193 	 *
194 	 * @return the address connections are received on.
195 	 */
196 	public synchronized InetSocketAddress getAddress() {
197 		return myAddress;
198 	}
199 
200 	/**
201 	 * Lookup a supported service so it can be reconfigured.
202 	 *
203 	 * @param name
204 	 *            name of the service; e.g. "receive-pack"/"git-receive-pack" or
205 	 *            "upload-pack"/"git-upload-pack".
206 	 * @return the service; null if this daemon implementation doesn't support
207 	 *         the requested service type.
208 	 */
209 	public synchronized DaemonService getService(String name) {
210 		if (!name.startsWith("git-")) //$NON-NLS-1$
211 			name = "git-" + name; //$NON-NLS-1$
212 		for (DaemonService s : services) {
213 			if (s.getCommandName().equals(name))
214 				return s;
215 		}
216 		return null;
217 	}
218 
219 	/**
220 	 * Get timeout (in seconds) before aborting an IO operation.
221 	 *
222 	 * @return timeout (in seconds) before aborting an IO operation.
223 	 */
224 	public int getTimeout() {
225 		return timeout;
226 	}
227 
228 	/**
229 	 * Set the timeout before willing to abort an IO call.
230 	 *
231 	 * @param seconds
232 	 *            number of seconds to wait (with no data transfer occurring)
233 	 *            before aborting an IO read or write operation with the
234 	 *            connected client.
235 	 */
236 	public void setTimeout(int seconds) {
237 		timeout = seconds;
238 	}
239 
240 	/**
241 	 * Get configuration controlling packing, may be null.
242 	 *
243 	 * @return configuration controlling packing, may be null.
244 	 */
245 	public PackConfig getPackConfig() {
246 		return packConfig;
247 	}
248 
249 	/**
250 	 * Set the configuration used by the pack generator.
251 	 *
252 	 * @param pc
253 	 *            configuration controlling packing parameters. If null the
254 	 *            source repository's settings will be used.
255 	 */
256 	public void setPackConfig(PackConfig pc) {
257 		this.packConfig = pc;
258 	}
259 
260 	/**
261 	 * Set the resolver used to locate a repository by name.
262 	 *
263 	 * @param resolver
264 	 *            the resolver instance.
265 	 */
266 	public void setRepositoryResolver(RepositoryResolver<DaemonClient> resolver) {
267 		repositoryResolver = resolver;
268 	}
269 
270 	/**
271 	 * Set the factory to construct and configure per-request UploadPack.
272 	 *
273 	 * @param factory
274 	 *            the factory. If null upload-pack is disabled.
275 	 */
276 	@SuppressWarnings("unchecked")
277 	public void setUploadPackFactory(UploadPackFactory<DaemonClient> factory) {
278 		if (factory != null)
279 			uploadPackFactory = factory;
280 		else
281 			uploadPackFactory = (UploadPackFactory<DaemonClient>) UploadPackFactory.DISABLED;
282 	}
283 
284 	/**
285 	 * Get the factory used to construct per-request ReceivePack.
286 	 *
287 	 * @return the factory.
288 	 * @since 4.3
289 	 */
290 	public ReceivePackFactory<DaemonClient> getReceivePackFactory() {
291 		return receivePackFactory;
292 	}
293 
294 	/**
295 	 * Set the factory to construct and configure per-request ReceivePack.
296 	 *
297 	 * @param factory
298 	 *            the factory. If null receive-pack is disabled.
299 	 */
300 	@SuppressWarnings("unchecked")
301 	public void setReceivePackFactory(ReceivePackFactory<DaemonClient> factory) {
302 		if (factory != null)
303 			receivePackFactory = factory;
304 		else
305 			receivePackFactory = (ReceivePackFactory<DaemonClient>) ReceivePackFactory.DISABLED;
306 	}
307 
308 	private class Acceptor extends Thread {
309 
310 		private final ServerSocket listenSocket;
311 
312 		private final AtomicBoolean running = new AtomicBoolean(true);
313 
314 		public Acceptor(ThreadGroup group, String name, ServerSocket socket) {
315 			super(group, name);
316 			this.listenSocket = socket;
317 		}
318 
319 		@Override
320 		public void run() {
321 			setUncaughtExceptionHandler((thread, throwable) -> terminate());
322 			while (isRunning()) {
323 				try {
324 					startClient(listenSocket.accept());
325 				} catch (SocketException e) {
326 					// Test again to see if we should keep accepting.
327 				} catch (IOException e) {
328 					break;
329 				}
330 			}
331 
332 			terminate();
333 		}
334 
335 		private void terminate() {
336 			try {
337 				shutDown();
338 			} finally {
339 				clearThread();
340 			}
341 		}
342 
343 		public boolean isRunning() {
344 			return running.get();
345 		}
346 
347 		public void shutDown() {
348 			running.set(false);
349 			try {
350 				listenSocket.close();
351 			} catch (IOException err) {
352 				//
353 			}
354 		}
355 
356 	}
357 
358 	/**
359 	 * Start this daemon on a background thread.
360 	 *
361 	 * @throws java.io.IOException
362 	 *             the server socket could not be opened.
363 	 * @throws java.lang.IllegalStateException
364 	 *             the daemon is already running.
365 	 */
366 	public synchronized void start() throws IOException {
367 		if (acceptThread != null) {
368 			throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
369 		}
370 		ServerSocket socket = new ServerSocket();
371 		socket.setReuseAddress(true);
372 		if (myAddress != null) {
373 			socket.bind(myAddress, BACKLOG);
374 		} else {
375 			socket.bind(new InetSocketAddress((InetAddress) null, 0), BACKLOG);
376 		}
377 		myAddress = (InetSocketAddress) socket.getLocalSocketAddress();
378 
379 		acceptThread = new Acceptor(processors, "Git-Daemon-Accept", socket); //$NON-NLS-1$
380 		acceptThread.start();
381 	}
382 
383 	private synchronized void clearThread() {
384 		acceptThread = null;
385 	}
386 
387 	/**
388 	 * Whether this daemon is receiving connections.
389 	 *
390 	 * @return {@code true} if this daemon is receiving connections.
391 	 */
392 	public synchronized boolean isRunning() {
393 		return acceptThread != null && acceptThread.isRunning();
394 	}
395 
396 	/**
397 	 * Stop this daemon.
398 	 */
399 	public synchronized void stop() {
400 		if (acceptThread != null) {
401 			acceptThread.shutDown();
402 		}
403 	}
404 
405 	/**
406 	 * Stops this daemon and waits until it's acceptor thread has finished.
407 	 *
408 	 * @throws java.lang.InterruptedException
409 	 *             if waiting for the acceptor thread is interrupted
410 	 * @since 4.9
411 	 */
412 	public void stopAndWait() throws InterruptedException {
413 		Thread acceptor = null;
414 		synchronized (this) {
415 			acceptor = acceptThread;
416 			stop();
417 		}
418 		if (acceptor != null) {
419 			acceptor.join();
420 		}
421 	}
422 
423 	void startClient(Socket s) {
424 		final DaemonClient dc = new DaemonClient(this);
425 
426 		final SocketAddress peer = s.getRemoteSocketAddress();
427 		if (peer instanceof InetSocketAddress)
428 			dc.setRemoteAddress(((InetSocketAddress) peer).getAddress());
429 
430 		new Thread(processors, "Git-Daemon-Client " + peer.toString()) { //$NON-NLS-1$
431 			@Override
432 			public void run() {
433 				try {
434 					dc.execute(s);
435 				} catch (ServiceNotEnabledException e) {
436 					// Ignored. Client cannot use this repository.
437 				} catch (ServiceNotAuthorizedException e) {
438 					// Ignored. Client cannot use this repository.
439 				} catch (IOException e) {
440 					// Ignore unexpected IO exceptions from clients
441 				} finally {
442 					try {
443 						s.getInputStream().close();
444 					} catch (IOException e) {
445 						// Ignore close exceptions
446 					}
447 					try {
448 						s.getOutputStream().close();
449 					} catch (IOException e) {
450 						// Ignore close exceptions
451 					}
452 				}
453 			}
454 		}.start();
455 	}
456 
457 	synchronized DaemonService matchService(String cmd) {
458 		for (DaemonService d : services) {
459 			if (d.handles(cmd))
460 				return d;
461 		}
462 		return null;
463 	}
464 
465 	Repository openRepository(DaemonClient client, String name)
466 			throws ServiceMayNotContinueException {
467 		// Assume any attempt to use \ was by a Windows client
468 		// and correct to the more typical / used in Git URIs.
469 		//
470 		name = name.replace('\\', '/');
471 
472 		// git://thishost/path should always be name="/path" here
473 		//
474 		if (!name.startsWith("/")) //$NON-NLS-1$
475 			return null;
476 
477 		try {
478 			return repositoryResolver.open(client, name.substring(1));
479 		} catch (RepositoryNotFoundException e) {
480 			// null signals it "wasn't found", which is all that is suitable
481 			// for the remote client to know.
482 			return null;
483 		} catch (ServiceNotAuthorizedException e) {
484 			// null signals it "wasn't found", which is all that is suitable
485 			// for the remote client to know.
486 			return null;
487 		} catch (ServiceNotEnabledException e) {
488 			// null signals it "wasn't found", which is all that is suitable
489 			// for the remote client to know.
490 			return null;
491 		}
492 	}
493 }