Daemon.java

  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. package org.eclipse.jgit.transport;

  11. import java.io.IOException;
  12. import java.io.InputStream;
  13. import java.io.OutputStream;
  14. import java.net.InetAddress;
  15. import java.net.InetSocketAddress;
  16. import java.net.ServerSocket;
  17. import java.net.Socket;
  18. import java.net.SocketAddress;
  19. import java.net.SocketException;
  20. import java.util.concurrent.atomic.AtomicBoolean;
  21. import java.util.Collection;

  22. import org.eclipse.jgit.annotations.Nullable;
  23. import org.eclipse.jgit.errors.RepositoryNotFoundException;
  24. import org.eclipse.jgit.internal.JGitText;
  25. import org.eclipse.jgit.lib.PersonIdent;
  26. import org.eclipse.jgit.lib.Repository;
  27. import org.eclipse.jgit.storage.pack.PackConfig;
  28. import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
  29. import org.eclipse.jgit.transport.resolver.RepositoryResolver;
  30. import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
  31. import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
  32. import org.eclipse.jgit.transport.resolver.UploadPackFactory;

  33. /**
  34.  * Basic daemon for the anonymous <code>git://</code> transport protocol.
  35.  */
  36. public class Daemon {
  37.     /** 9418: IANA assigned port number for Git. */
  38.     public static final int DEFAULT_PORT = 9418;

  39.     private static final int BACKLOG = 5;

  40.     private InetSocketAddress myAddress;

  41.     private final DaemonService[] services;

  42.     private final ThreadGroup processors;

  43.     private Acceptor acceptThread;

  44.     private int timeout;

  45.     private PackConfig packConfig;

  46.     private volatile RepositoryResolver<DaemonClient> repositoryResolver;

  47.     volatile UploadPackFactory<DaemonClient> uploadPackFactory;

  48.     volatile ReceivePackFactory<DaemonClient> receivePackFactory;

  49.     /**
  50.      * Configure a daemon to listen on any available network port.
  51.      */
  52.     public Daemon() {
  53.         this(null);
  54.     }

  55.     /**
  56.      * Configure a new daemon for the specified network address.
  57.      *
  58.      * @param addr
  59.      *            address to listen for connections on. If null, any available
  60.      *            port will be chosen on all network interfaces.
  61.      */
  62.     @SuppressWarnings("unchecked")
  63.     public Daemon(InetSocketAddress addr) {
  64.         myAddress = addr;
  65.         processors = new ThreadGroup("Git-Daemon"); //$NON-NLS-1$

  66.         repositoryResolver = (RepositoryResolver<DaemonClient>) RepositoryResolver.NONE;

  67.         uploadPackFactory = (DaemonClient req, Repository db) -> {
  68.             UploadPack up = new UploadPack(db);
  69.             up.setTimeout(getTimeout());
  70.             up.setPackConfig(getPackConfig());
  71.             return up;
  72.         };

  73.         receivePackFactory = (DaemonClient req, Repository db) -> {
  74.             ReceivePack rp = new ReceivePack(db);

  75.             InetAddress peer = req.getRemoteAddress();
  76.             String host = peer.getCanonicalHostName();
  77.             if (host == null)
  78.                 host = peer.getHostAddress();
  79.             String name = "anonymous"; //$NON-NLS-1$
  80.             String email = name + "@" + host; //$NON-NLS-1$
  81.             rp.setRefLogIdent(new PersonIdent(name, email));
  82.             rp.setTimeout(getTimeout());

  83.             return rp;
  84.         };

  85.         services = new DaemonService[] {
  86.                 new DaemonService("upload-pack", "uploadpack") { //$NON-NLS-1$ //$NON-NLS-2$
  87.                     {
  88.                         setEnabled(true);
  89.                     }

  90.                     @Override
  91.                     protected void execute(final DaemonClient dc,
  92.                             final Repository db,
  93.                             @Nullable Collection<String> extraParameters)
  94.                             throws IOException,
  95.                             ServiceNotEnabledException,
  96.                             ServiceNotAuthorizedException {
  97.                         UploadPack up = uploadPackFactory.create(dc, db);
  98.                         InputStream in = dc.getInputStream();
  99.                         OutputStream out = dc.getOutputStream();
  100.                         if (extraParameters != null) {
  101.                             up.setExtraParameters(extraParameters);
  102.                         }
  103.                         up.upload(in, out, null);
  104.                     }
  105.                 }, new DaemonService("receive-pack", "receivepack") { //$NON-NLS-1$ //$NON-NLS-2$
  106.                     {
  107.                         setEnabled(false);
  108.                     }

  109.                     @Override
  110.                     protected void execute(final DaemonClient dc,
  111.                             final Repository db,
  112.                             @Nullable Collection<String> extraParameters)
  113.                             throws IOException,
  114.                             ServiceNotEnabledException,
  115.                             ServiceNotAuthorizedException {
  116.                         ReceivePack rp = receivePackFactory.create(dc, db);
  117.                         InputStream in = dc.getInputStream();
  118.                         OutputStream out = dc.getOutputStream();
  119.                         rp.receive(in, out, null);
  120.                     }
  121.                 } };
  122.     }

  123.     /**
  124.      * Get the address connections are received on.
  125.      *
  126.      * @return the address connections are received on.
  127.      */
  128.     public synchronized InetSocketAddress getAddress() {
  129.         return myAddress;
  130.     }

  131.     /**
  132.      * Lookup a supported service so it can be reconfigured.
  133.      *
  134.      * @param name
  135.      *            name of the service; e.g. "receive-pack"/"git-receive-pack" or
  136.      *            "upload-pack"/"git-upload-pack".
  137.      * @return the service; null if this daemon implementation doesn't support
  138.      *         the requested service type.
  139.      */
  140.     public synchronized DaemonService getService(String name) {
  141.         if (!name.startsWith("git-")) //$NON-NLS-1$
  142.             name = "git-" + name; //$NON-NLS-1$
  143.         for (DaemonService s : services) {
  144.             if (s.getCommandName().equals(name))
  145.                 return s;
  146.         }
  147.         return null;
  148.     }

  149.     /**
  150.      * Get timeout (in seconds) before aborting an IO operation.
  151.      *
  152.      * @return timeout (in seconds) before aborting an IO operation.
  153.      */
  154.     public int getTimeout() {
  155.         return timeout;
  156.     }

  157.     /**
  158.      * Set the timeout before willing to abort an IO call.
  159.      *
  160.      * @param seconds
  161.      *            number of seconds to wait (with no data transfer occurring)
  162.      *            before aborting an IO read or write operation with the
  163.      *            connected client.
  164.      */
  165.     public void setTimeout(int seconds) {
  166.         timeout = seconds;
  167.     }

  168.     /**
  169.      * Get configuration controlling packing, may be null.
  170.      *
  171.      * @return configuration controlling packing, may be null.
  172.      */
  173.     public PackConfig getPackConfig() {
  174.         return packConfig;
  175.     }

  176.     /**
  177.      * Set the configuration used by the pack generator.
  178.      *
  179.      * @param pc
  180.      *            configuration controlling packing parameters. If null the
  181.      *            source repository's settings will be used.
  182.      */
  183.     public void setPackConfig(PackConfig pc) {
  184.         this.packConfig = pc;
  185.     }

  186.     /**
  187.      * Set the resolver used to locate a repository by name.
  188.      *
  189.      * @param resolver
  190.      *            the resolver instance.
  191.      */
  192.     public void setRepositoryResolver(RepositoryResolver<DaemonClient> resolver) {
  193.         repositoryResolver = resolver;
  194.     }

  195.     /**
  196.      * Set the factory to construct and configure per-request UploadPack.
  197.      *
  198.      * @param factory
  199.      *            the factory. If null upload-pack is disabled.
  200.      */
  201.     @SuppressWarnings("unchecked")
  202.     public void setUploadPackFactory(UploadPackFactory<DaemonClient> factory) {
  203.         if (factory != null)
  204.             uploadPackFactory = factory;
  205.         else
  206.             uploadPackFactory = (UploadPackFactory<DaemonClient>) UploadPackFactory.DISABLED;
  207.     }

  208.     /**
  209.      * Get the factory used to construct per-request ReceivePack.
  210.      *
  211.      * @return the factory.
  212.      * @since 4.3
  213.      */
  214.     public ReceivePackFactory<DaemonClient> getReceivePackFactory() {
  215.         return receivePackFactory;
  216.     }

  217.     /**
  218.      * Set the factory to construct and configure per-request ReceivePack.
  219.      *
  220.      * @param factory
  221.      *            the factory. If null receive-pack is disabled.
  222.      */
  223.     @SuppressWarnings("unchecked")
  224.     public void setReceivePackFactory(ReceivePackFactory<DaemonClient> factory) {
  225.         if (factory != null)
  226.             receivePackFactory = factory;
  227.         else
  228.             receivePackFactory = (ReceivePackFactory<DaemonClient>) ReceivePackFactory.DISABLED;
  229.     }

  230.     private class Acceptor extends Thread {

  231.         private final ServerSocket listenSocket;

  232.         private final AtomicBoolean running = new AtomicBoolean(true);

  233.         public Acceptor(ThreadGroup group, String name, ServerSocket socket) {
  234.             super(group, name);
  235.             this.listenSocket = socket;
  236.         }

  237.         @Override
  238.         public void run() {
  239.             setUncaughtExceptionHandler((thread, throwable) -> terminate());
  240.             while (isRunning()) {
  241.                 try {
  242.                     startClient(listenSocket.accept());
  243.                 } catch (SocketException e) {
  244.                     // Test again to see if we should keep accepting.
  245.                 } catch (IOException e) {
  246.                     break;
  247.                 }
  248.             }

  249.             terminate();
  250.         }

  251.         private void terminate() {
  252.             try {
  253.                 shutDown();
  254.             } finally {
  255.                 clearThread();
  256.             }
  257.         }

  258.         public boolean isRunning() {
  259.             return running.get();
  260.         }

  261.         public void shutDown() {
  262.             running.set(false);
  263.             try {
  264.                 listenSocket.close();
  265.             } catch (IOException err) {
  266.                 //
  267.             }
  268.         }

  269.     }

  270.     /**
  271.      * Start this daemon on a background thread.
  272.      *
  273.      * @throws java.io.IOException
  274.      *             the server socket could not be opened.
  275.      * @throws java.lang.IllegalStateException
  276.      *             the daemon is already running.
  277.      */
  278.     public synchronized void start() throws IOException {
  279.         if (acceptThread != null) {
  280.             throw new IllegalStateException(JGitText.get().daemonAlreadyRunning);
  281.         }
  282.         ServerSocket socket = new ServerSocket();
  283.         socket.setReuseAddress(true);
  284.         if (myAddress != null) {
  285.             socket.bind(myAddress, BACKLOG);
  286.         } else {
  287.             socket.bind(new InetSocketAddress((InetAddress) null, 0), BACKLOG);
  288.         }
  289.         myAddress = (InetSocketAddress) socket.getLocalSocketAddress();

  290.         acceptThread = new Acceptor(processors, "Git-Daemon-Accept", socket); //$NON-NLS-1$
  291.         acceptThread.start();
  292.     }

  293.     private synchronized void clearThread() {
  294.         acceptThread = null;
  295.     }

  296.     /**
  297.      * Whether this daemon is receiving connections.
  298.      *
  299.      * @return {@code true} if this daemon is receiving connections.
  300.      */
  301.     public synchronized boolean isRunning() {
  302.         return acceptThread != null && acceptThread.isRunning();
  303.     }

  304.     /**
  305.      * Stop this daemon.
  306.      */
  307.     public synchronized void stop() {
  308.         if (acceptThread != null) {
  309.             acceptThread.shutDown();
  310.         }
  311.     }

  312.     /**
  313.      * Stops this daemon and waits until it's acceptor thread has finished.
  314.      *
  315.      * @throws java.lang.InterruptedException
  316.      *             if waiting for the acceptor thread is interrupted
  317.      * @since 4.9
  318.      */
  319.     public void stopAndWait() throws InterruptedException {
  320.         Thread acceptor = null;
  321.         synchronized (this) {
  322.             acceptor = acceptThread;
  323.             stop();
  324.         }
  325.         if (acceptor != null) {
  326.             acceptor.join();
  327.         }
  328.     }

  329.     void startClient(Socket s) {
  330.         final DaemonClient dc = new DaemonClient(this);

  331.         final SocketAddress peer = s.getRemoteSocketAddress();
  332.         if (peer instanceof InetSocketAddress)
  333.             dc.setRemoteAddress(((InetSocketAddress) peer).getAddress());

  334.         new Thread(processors, "Git-Daemon-Client " + peer.toString()) { //$NON-NLS-1$
  335.             @Override
  336.             public void run() {
  337.                 try {
  338.                     dc.execute(s);
  339.                 } catch (ServiceNotEnabledException e) {
  340.                     // Ignored. Client cannot use this repository.
  341.                 } catch (ServiceNotAuthorizedException e) {
  342.                     // Ignored. Client cannot use this repository.
  343.                 } catch (IOException e) {
  344.                     // Ignore unexpected IO exceptions from clients
  345.                 } finally {
  346.                     try {
  347.                         s.getInputStream().close();
  348.                     } catch (IOException e) {
  349.                         // Ignore close exceptions
  350.                     }
  351.                     try {
  352.                         s.getOutputStream().close();
  353.                     } catch (IOException e) {
  354.                         // Ignore close exceptions
  355.                     }
  356.                 }
  357.             }
  358.         }.start();
  359.     }

  360.     synchronized DaemonService matchService(String cmd) {
  361.         for (DaemonService d : services) {
  362.             if (d.handles(cmd))
  363.                 return d;
  364.         }
  365.         return null;
  366.     }

  367.     Repository openRepository(DaemonClient client, String name)
  368.             throws ServiceMayNotContinueException {
  369.         // Assume any attempt to use \ was by a Windows client
  370.         // and correct to the more typical / used in Git URIs.
  371.         //
  372.         name = name.replace('\\', '/');

  373.         // git://thishost/path should always be name="/path" here
  374.         //
  375.         if (!name.startsWith("/")) //$NON-NLS-1$
  376.             return null;

  377.         try {
  378.             return repositoryResolver.open(client, name.substring(1));
  379.         } catch (RepositoryNotFoundException e) {
  380.             // null signals it "wasn't found", which is all that is suitable
  381.             // for the remote client to know.
  382.             return null;
  383.         } catch (ServiceNotAuthorizedException e) {
  384.             // null signals it "wasn't found", which is all that is suitable
  385.             // for the remote client to know.
  386.             return null;
  387.         } catch (ServiceNotEnabledException e) {
  388.             // null signals it "wasn't found", which is all that is suitable
  389.             // for the remote client to know.
  390.             return null;
  391.         }
  392.     }
  393. }