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