Move sshd_port to gerrit.config as sshd.listenaddress We now support not only the port to listen on, but also the IP address (or addresses) the daemon should listen on. This may help sites which host multiple installations on one system, by binding each SSHD onto a logical IP address. Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index d5cc6a4..5534725 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -210,6 +210,24 @@ Section sshd ~~~~~~~~~~~~ +sshd.listenAddress:: ++ +Specifies the local addresses the internal SSHD should listen +for connections on. The following forms may be used to specify +an address. In any form, `:'port'` may be omitted to use the +default of 29418. ++ +* 'hostname':'port' (for example `review.example.com:29418`) +* 'IPv4':'port' (for example `10.0.0.1:29418`) +* ['IPv6']:'port' (for example `[ff02::1]:29418`) +* \*:'port' (for example `*:29418`) + ++ +If multiple values are supplied, the daemon will listen on all +of them. ++ +By default, *:29418. + sshd.reuseAddress:: + If true, permits the daemon to bind to the port even if the port @@ -384,15 +402,6 @@ + * link:config-contact.html[Contact Information] -sshd_port:: -+ -Port number the internal SSHD listens for connections on. -+ -Gerrit receives new change submissions through this port by -"git push ssh://you@example.com:$sshd_port/$project.git ...". -+ -By default this is 29418. - login_type:: + Type of user authentication employed by Gerrit. This setting has
diff --git a/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java b/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java index 918f5ac..635d26f 100644 --- a/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java +++ b/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
@@ -210,13 +210,15 @@ // Use our SSH daemon URL as its the only way they can get // to the project (that we know of anyway). // + final String sshAddr = Common.getGerritConfig().getSshdAddress(); final StringBuilder r = new StringBuilder(); r.append("git pull ssh://"); r.append(Gerrit.getUserAccount().getSshUserName()); r.append("@"); - r.append(Window.Location.getHostName()); - r.append(":"); - r.append(Common.getGerritConfig().getSshdPort()); + if (sshAddr.startsWith(":") || "".equals(sshAddr)) { + r.append(Window.Location.getHostName()); + } + r.append(sshAddr); r.append("/"); r.append(projectName); r.append(" ");
diff --git a/src/main/java/com/google/gerrit/client/data/GerritConfig.java b/src/main/java/com/google/gerrit/client/data/GerritConfig.java index f35aee4..d6d1eec 100644 --- a/src/main/java/com/google/gerrit/client/data/GerritConfig.java +++ b/src/main/java/com/google/gerrit/client/data/GerritConfig.java
@@ -22,17 +22,17 @@ import java.util.List; import java.util.Map; -public class GerritConfig { +public class GerritConfig implements Cloneable { protected String canonicalUrl; protected GitwebLink gitweb; protected List<ApprovalType> approvalTypes; protected List<ApprovalType> actionTypes; - protected int sshdPort; protected boolean useContributorAgreements; protected boolean useContactInfo; protected SystemConfig.LoginType loginType; protected boolean useRepoDownload; protected String gitDaemonUrl; + protected String sshdAddress; private transient Map<ApprovalCategory.Id, ApprovalType> byCategoryId; public GerritConfig() { @@ -94,14 +94,6 @@ } } - public int getSshdPort() { - return sshdPort; - } - - public void setSshdPort(final int p) { - sshdPort = p; - } - public boolean isUseContributorAgreements() { return useContributorAgreements; } @@ -154,4 +146,12 @@ } gitDaemonUrl = url; } + + public String getSshdAddress() { + return sshdAddress; + } + + public void setSshdAddress(final String addr) { + sshdAddress = addr; + } }
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java b/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java index c529389..15ac6c6 100644 --- a/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java +++ b/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
@@ -31,7 +31,7 @@ * </ul> */ public interface ReviewDb extends Schema { - public static final int VERSION = 11; + public static final int VERSION = 12; @Relation SchemaVersionAccess schemaVersion();
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java b/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java index 5bafe68..461d92a 100644 --- a/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java +++ b/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
@@ -142,10 +142,6 @@ @Column public boolean useContributorAgreements; - /** Local TCP port number the embedded SSHD server binds onto. */ - @Column - public int sshdPort; - /** Should Gerrit advertise 'repo download' for patch sets? */ @Column public boolean useRepoDownload;
diff --git a/src/main/java/com/google/gerrit/server/GerritServer.java b/src/main/java/com/google/gerrit/server/GerritServer.java index ecaaecc..97c0813 100644 --- a/src/main/java/com/google/gerrit/server/GerritServer.java +++ b/src/main/java/com/google/gerrit/server/GerritServer.java
@@ -435,7 +435,6 @@ s.maxSessionAge = 12 * 60 * 60 /* seconds */; s.xsrfPrivateKey = SignedToken.generateRandomKey(); s.accountPrivateKey = SignedToken.generateRandomKey(); - s.sshdPort = 29418; s.adminGroupId = admin.getId(); s.anonymousGroupId = anonymous.getId(); s.registeredGroupId = registered.getId(); @@ -666,7 +665,6 @@ private void loadGerritConfig(final ReviewDb db) throws OrmException { final GerritConfig r = new GerritConfig(); r.setCanonicalUrl(getCanonicalURL()); - r.setSshdPort(sConfig.sshdPort); r.setUseContributorAgreements(sConfig.useContributorAgreements); r.setGitDaemonUrl(sConfig.gitDaemonUrl); r.setUseRepoDownload(sConfig.useRepoDownload);
diff --git a/src/main/java/com/google/gerrit/server/HostPageServlet.java b/src/main/java/com/google/gerrit/server/HostPageServlet.java index d622da0..1ab66f8 100644 --- a/src/main/java/com/google/gerrit/server/HostPageServlet.java +++ b/src/main/java/com/google/gerrit/server/HostPageServlet.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server; +import com.google.gerrit.client.data.GerritConfig; import com.google.gerrit.client.reviewdb.Account; import com.google.gerrit.client.rpc.Common; import com.google.gwt.user.server.rpc.RPCServletUtils; @@ -43,12 +44,10 @@ public class HostPageServlet extends HttpServlet { private GerritServer server; private String canonicalUrl; - private byte[] hostPageRaw; - private byte[] hostPageCompressed; private Document hostDoc; @Override - public void init(final ServletConfig config) throws ServletException { + public void init(ServletConfig config) throws ServletException { super.init(config); try { @@ -68,19 +67,9 @@ throw new ServletException("No " + hostPageName + " in webapp"); } fixModuleReference(hostDoc); - injectJson(hostDoc, "gerrit_gerritconfig", Common.getGerritConfig()); injectCssFile(hostDoc, "gerrit_sitecss", sitePath, "GerritSite.css"); injectXmlFile(hostDoc, "gerrit_header", sitePath, "GerritSiteHeader.html"); injectXmlFile(hostDoc, "gerrit_footer", sitePath, "GerritSiteFooter.html"); - - try { - final Document anon = HtmlDomUtil.clone(hostDoc); - injectJson(anon, "gerrit_myaccount", null); - hostPageRaw = HtmlDomUtil.toUTF8(anon); - hostPageCompressed = HtmlDomUtil.compress(hostPageRaw); - } catch (IOException e) { - throw new ServletException(e.getMessage(), e); - } } private void injectXmlFile(final Document hostDoc, final String id, @@ -190,7 +179,6 @@ @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) throws IOException { - // If we get a request for "/Gerrit/change,1" rewrite it the way // it should have been, as "/Gerrit#change,1". This may happen // coming out of Google Analytics, where its common to replace @@ -220,30 +208,19 @@ final Account.Id me = new GerritCall(server, req, rsp).getAccountId(); final Account account = Common.getAccountCache().get(me); - final byte[] tosend; - if (account != null) { - // We know who the user is; embed their account data into the host - // page to avoid an RPC during module loading. - // - final Document peruser = HtmlDomUtil.clone(hostDoc); - injectJson(peruser, "gerrit_myaccount", account); - final byte[] raw = HtmlDomUtil.toUTF8(peruser); - if (RPCServletUtils.acceptsGzipEncoding(req)) { - rsp.setHeader("Content-Encoding", "gzip"); - tosend = HtmlDomUtil.compress(raw); - } else { - tosend = raw; - } + final GerritConfig config = SystemInfoServiceImpl.getGerritConfig(); + final Document peruser = HtmlDomUtil.clone(hostDoc); + injectJson(peruser, "gerrit_gerritconfig", config); + injectJson(peruser, "gerrit_myaccount", account); + + final byte[] raw = HtmlDomUtil.toUTF8(peruser); + final byte[] tosend; + if (RPCServletUtils.acceptsGzipEncoding(req)) { + rsp.setHeader("Content-Encoding", "gzip"); + tosend = HtmlDomUtil.compress(raw); } else { - // User is anonymous (hasn't authenticated with us). - // - if (RPCServletUtils.acceptsGzipEncoding(req)) { - rsp.setHeader("Content-Encoding", "gzip"); - tosend = hostPageCompressed; - } else { - tosend = hostPageRaw; - } + tosend = raw; } rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
diff --git a/src/main/java/com/google/gerrit/server/SystemInfoServiceImpl.java b/src/main/java/com/google/gerrit/server/SystemInfoServiceImpl.java index f6ded8f..69d9959 100644 --- a/src/main/java/com/google/gerrit/server/SystemInfoServiceImpl.java +++ b/src/main/java/com/google/gerrit/server/SystemInfoServiceImpl.java
@@ -32,6 +32,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; import java.security.PublicKey; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; @@ -47,8 +51,33 @@ LoggerFactory.getLogger(SystemInfoServiceImpl.class); private static final JSch JSCH = new JSch(); + public static GerritConfig getGerritConfig() { + final GerritConfig cfg = Common.getGerritConfig(); + synchronized (cfg) { + if (cfg.getSshdAddress() == null) { + final InetSocketAddress addr = GerritSshDaemon.getAddress(); + if (addr != null) { + final InetAddress ip = addr.getAddress(); + String host; + if (ip != null && ip.isAnyLocalAddress()) { + host = ""; + } else if (ip instanceof Inet6Address) { + host = "[" + addr.getHostName() + "]"; + } else { + host = addr.getHostName(); + } + if (addr.getPort() != 22) { + host += ":" + addr.getPort(); + } + cfg.setSshdAddress(host); + } + } + } + return cfg; + } + public void loadGerritConfig(final AsyncCallback<GerritConfig> callback) { - callback.onSuccess(Common.getGerritConfig()); + callback.onSuccess(getGerritConfig()); } public void contributorAgreements( @@ -114,8 +143,21 @@ private String hostIdent() { final HttpServletRequest req = GerritJsonServlet.getCurrentCall().getHttpServletRequest(); - final String serverName = req.getServerName(); - final int serverPort = GerritSshDaemon.getSshdPort(); - return serverPort == 22 ? serverName : "[" + serverName + "]:" + serverPort; + + InetSocketAddress addr = GerritSshDaemon.getAddress(); + if (addr.getAddress() != null && addr.getAddress().isAnyLocalAddress()) { + final InetAddress me; + try { + me = InetAddress.getByName(req.getLocalAddr()); + } catch (UnknownHostException e) { + throw new RuntimeException("Unexpected uknown host", e); + } + addr = new InetSocketAddress(me, addr.getPort()); + } + + if (addr.getPort() == 22 && !(addr.getAddress() instanceof Inet6Address)) { + return addr.getHostName(); + } + return "[" + addr.getHostName() + "]:" + addr.getPort(); } }
diff --git a/src/main/java/com/google/gerrit/server/ssh/GerritSshDaemon.java b/src/main/java/com/google/gerrit/server/ssh/GerritSshDaemon.java index 86f9660..48db90c 100644 --- a/src/main/java/com/google/gerrit/server/ssh/GerritSshDaemon.java +++ b/src/main/java/com/google/gerrit/server/ssh/GerritSshDaemon.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.ssh; -import com.google.gerrit.client.rpc.Common; import com.google.gerrit.server.GerritServer; import com.google.gwtjsonrpc.server.XsrfException; import com.google.gwtorm.client.OrmException; @@ -62,14 +61,18 @@ import java.io.File; import java.io.IOException; +import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.net.SocketException; +import java.net.UnknownHostException; import java.security.KeyPair; import java.security.PublicKey; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; /** @@ -90,30 +93,40 @@ * </pre> */ public class GerritSshDaemon extends SshServer { + private static final int DEFAULT_PORT = 29418; + private static final Logger log = LoggerFactory.getLogger(GerritSshDaemon.class); private static GerritSshDaemon sshd; + private static InetSocketAddress preferredAddress; private static Collection<PublicKey> hostKeys = Collections.emptyList(); public static synchronized void startSshd() throws OrmException, XsrfException, SocketException { final GerritServer srv = GerritServer.getInstance(); final GerritSshDaemon daemon = new GerritSshDaemon(srv); + final String addressList = daemon.addressList(); try { sshd = daemon; + preferredAddress = null; hostKeys = computeHostKeys(); + if (hostKeys.isEmpty()) { throw new IOException("No SSHD host key"); } daemon.start(); - log.info("Started Gerrit SSHD on 0.0.0.0:" + daemon.getPort()); + + log.info("Started Gerrit SSHD on " + addressList); } catch (IOException e) { - log.error("Cannot start Gerrit SSHD on 0.0.0.0:" + daemon.getPort(), e); sshd = null; + preferredAddress = null; hostKeys = Collections.emptyList(); + + final String msg = "Cannot start Gerrit SSHD on " + addressList; + log.error(msg, e); final SocketException e2; - e2 = new SocketException("Cannot start sshd on " + daemon.getPort()); + e2 = new SocketException(msg); e2.initCause(e); throw e2; } @@ -135,22 +148,34 @@ } } + private static String format(final SocketAddress addr) { + if (addr instanceof InetSocketAddress) { + final InetSocketAddress inetAddr = (InetSocketAddress) addr; + final InetAddress hostAddr = inetAddr.getAddress(); + String host; + if (hostAddr.isAnyLocalAddress()) { + host = "*"; + } else { + host = "[" + hostAddr.getCanonicalHostName() + "]"; + } + return host + ":" + inetAddr.getPort(); + } + return addr.toString(); + } + public static synchronized void stopSshd() { if (sshd != null) { try { sshd.stop(); - log.info("Stopped Gerrit SSHD on 0.0.0.0:" + sshd.getPort()); + log.info("Stopped Gerrit SSHD"); } finally { sshd = null; + preferredAddress = null; hostKeys = Collections.emptyList(); } } } - public static synchronized int getSshdPort() { - return sshd != null ? sshd.getPort() : 0; - } - public static synchronized IoAcceptor getIoAcceptor() { return sshd != null ? sshd.acceptor : null; } @@ -159,14 +184,46 @@ return hostKeys; } + public static synchronized InetSocketAddress getAddress() { + if (sshd != null && preferredAddress == null) { + preferredAddress = computePreferredAddress(); + } + return preferredAddress; + } + + private static InetSocketAddress computePreferredAddress() { + for (final SocketAddress addr : sshd.listen) { + if (!(addr instanceof InetSocketAddress)) { + continue; + } + + InetSocketAddress inetAddr = (InetSocketAddress) addr; + if (inetAddr.getAddress().isLoopbackAddress()) { + continue; + } + if (inetAddr.getAddress().isAnyLocalAddress()) { + return inetAddr; + } + + String host = inetAddr.getAddress().getCanonicalHostName(); + if (host.equals(inetAddr.getAddress().getHostAddress())) { + return inetAddr; + } + return InetSocketAddress.createUnresolved(host, inetAddr.getPort()); + } + return null; + } + + private List<SocketAddress> listen; private IoAcceptor acceptor; private boolean reuseAddress; private boolean keepAlive; private GerritSshDaemon(final GerritServer srv) { - setPort(Common.getGerritConfig().getSshdPort()); + setPort(22/* never used */); final RepositoryConfig cfg = srv.getGerritConfig(); + listen = parseListen(cfg); reuseAddress = cfg.getBoolean("sshd", "reuseaddress", true); keepAlive = cfg.getBoolean("sshd", "tcpkeepalive", true); @@ -211,7 +268,7 @@ handler.setServer(this); ain.setHandler(handler); ain.setReuseAddress(reuseAddress); - ain.bind(new InetSocketAddress(getPort())); + ain.bind(listen); acceptor = ain; } } @@ -227,6 +284,87 @@ } } + private String addressList() { + final StringBuilder r = new StringBuilder(); + for (Iterator<SocketAddress> i = listen.iterator(); i.hasNext();) { + r.append(format(i.next())); + if (i.hasNext()) { + r.append(", "); + } + } + return r.toString(); + } + + private List<SocketAddress> parseListen(final RepositoryConfig cfg) { + final ArrayList<SocketAddress> bind = new ArrayList<SocketAddress>(2); + final String[] want = cfg.getStringList("sshd", null, "listenaddress"); + if (want == null || want.length == 0) { + bind.add(new InetSocketAddress(DEFAULT_PORT)); + return bind; + } + + for (final String desc : want) { + try { + bind.add(toSocketAddress(desc)); + } catch (IllegalArgumentException e) { + log.error("Bad sshd.listenaddress: " + desc + ": " + e.getMessage()); + } + } + + return bind; + } + + private SocketAddress toSocketAddress(final String desc) { + String hostStr; + String portStr; + + if (desc.startsWith("[")) { + // IPv6, as a raw IP address. + // + final int hostEnd = desc.indexOf(']'); + if (hostEnd < 0) { + throw new IllegalArgumentException("invalid IPv6 representation"); + } + + hostStr = desc.substring(1, hostEnd); + portStr = desc.substring(hostEnd + 1); + } else { + // IPv4, or a host name. + // + final int hostEnd = desc.indexOf(':'); + hostStr = 0 <= hostEnd ? desc.substring(0, hostEnd) : desc; + portStr = 0 <= hostEnd ? desc.substring(hostEnd) : ""; + } + + if ("*".equals(hostStr)) { + hostStr = ""; + } + if (portStr.startsWith(":")) { + portStr = portStr.substring(1); + } + + final int port; + if (portStr.length() > 0) { + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("invalid port"); + } + } else { + port = DEFAULT_PORT; + } + + if (hostStr.length() > 0) { + try { + return new InetSocketAddress(InetAddress.getByName(hostStr), port); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } else { + return new InetSocketAddress(port); + } + } + @SuppressWarnings("unchecked") private void initProviderBouncyCastle() { setKeyExchangeFactories(Arrays.<NamedFactory<KeyExchange>> asList( @@ -359,7 +497,7 @@ final File anyKey = new File(sitePath, "ssh_host_key"); final File rsaKey = new File(sitePath, "ssh_host_rsa_key"); final File dsaKey = new File(sitePath, "ssh_host_dsa_key"); - + final List<String> keys = new ArrayList<String>(2); if (rsaKey.exists()) { keys.add(rsaKey.getAbsolutePath()); @@ -370,14 +508,14 @@ if (anyKey.exists() && !keys.isEmpty()) { // If both formats of host key exist, we don't know which format - // should be authoritative. Complain and abort. + // should be authoritative. Complain and abort. // keys.add(anyKey.getAbsolutePath()); throw new IllegalStateException("Multiple host keys exist: " + keys); } if (keys.isEmpty()) { - // No administrator created host key? Generate and save our own. + // No administrator created host key? Generate and save our own. // final SimpleGeneratorHostKeyProvider keyp;
diff --git a/src/main/java/com/google/gerrit/server/ssh/SshServlet.java b/src/main/java/com/google/gerrit/server/ssh/SshServlet.java index 596db7a..97170b1 100644 --- a/src/main/java/com/google/gerrit/server/ssh/SshServlet.java +++ b/src/main/java/com/google/gerrit/server/ssh/SshServlet.java
@@ -19,7 +19,11 @@ import java.io.IOException; import java.io.PrintWriter; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.SocketException; +import java.net.UnknownHostException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -75,10 +79,19 @@ rsp.setHeader("Pragma", "no-cache"); rsp.setHeader("Cache-Control", "no-cache, must-revalidate"); - final int port = GerritSshDaemon.getSshdPort(); + final InetSocketAddress addr = GerritSshDaemon.getAddress(); final String out; - if (0 < port) { - out = req.getServerName() + " " + port; + if (addr != null) { + final InetAddress ip = addr.getAddress(); + String host; + if (ip != null && ip.isAnyLocalAddress()) { + host = req.getLocalAddr(); + } else if (ip instanceof Inet6Address) { + host = "[" + addr.getHostName() + "]"; + } else { + host = addr.getHostName(); + } + out = host + " " + addr.getPort(); } else { out = "NOT_AVAILABLE"; }
diff --git a/src/main/java/com/google/gerrit/server/ssh/SshUtil.java b/src/main/java/com/google/gerrit/server/ssh/SshUtil.java index 7fff352..a5d24db 100644 --- a/src/main/java/com/google/gerrit/server/ssh/SshUtil.java +++ b/src/main/java/com/google/gerrit/server/ssh/SshUtil.java
@@ -26,6 +26,8 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; +import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException;
diff --git a/src/main/webapp/WEB-INF/sql/upgrade011_012.sql b/src/main/webapp/WEB-INF/sql/upgrade011_012.sql new file mode 100644 index 0000000..e8b8e6d --- /dev/null +++ b/src/main/webapp/WEB-INF/sql/upgrade011_012.sql
@@ -0,0 +1,6 @@ +-- Upgrade: schema_version 11 to 12 +-- + +ALTER TABLE system_config DROP COLUMN sshd_port; + +UPDATE schema_version SET version_nbr = 12;