Line data Source code
1 : // Copyright (C) 2009 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.pgm.init;
16 :
17 : import static com.google.gerrit.common.FileUtil.chmod;
18 : import static com.google.gerrit.pgm.init.api.InitUtil.die;
19 : import static com.google.gerrit.pgm.init.api.InitUtil.domainOf;
20 : import static com.google.gerrit.pgm.init.api.InitUtil.isAnyAddress;
21 : import static com.google.gerrit.pgm.init.api.InitUtil.toURI;
22 :
23 : import com.google.gerrit.pgm.init.api.ConsoleUI;
24 : import com.google.gerrit.pgm.init.api.InitFlags;
25 : import com.google.gerrit.pgm.init.api.InitStep;
26 : import com.google.gerrit.pgm.init.api.Section;
27 : import com.google.gerrit.server.config.SitePaths;
28 : import com.google.gerrit.server.mail.SignedToken;
29 : import com.google.inject.Inject;
30 : import com.google.inject.Singleton;
31 : import java.io.IOException;
32 : import java.net.URI;
33 : import java.net.URISyntaxException;
34 : import java.nio.file.Files;
35 : import java.nio.file.Path;
36 :
37 : /** Initialize the {@code httpd} configuration section. */
38 : @Singleton
39 : class InitHttpd implements InitStep {
40 : private final ConsoleUI ui;
41 : private final SitePaths site;
42 : private final InitFlags flags;
43 : private final Section httpd;
44 : private final Section gerrit;
45 :
46 : @Inject
47 : InitHttpd(
48 : final ConsoleUI ui,
49 : final SitePaths site,
50 : final InitFlags flags,
51 15 : final Section.Factory sections) {
52 15 : this.ui = ui;
53 15 : this.site = site;
54 15 : this.flags = flags;
55 15 : this.httpd = sections.get("httpd", null);
56 15 : this.gerrit = sections.get("gerrit", null);
57 15 : }
58 :
59 : @Override
60 : public void run() throws IOException, InterruptedException {
61 15 : ui.header("HTTP Daemon");
62 :
63 15 : boolean anySsl = false;
64 : // If any listenUrls are present, validate whether it can be parsed as URL.
65 15 : String[] listenUrls = httpd.getList("listenUrl");
66 15 : for (String listenUrl : listenUrls) {
67 15 : if (listenUrl != null && !listenUrl.isEmpty()) {
68 : try {
69 15 : final URI u = toURI(listenUrl);
70 15 : if (u.getScheme().startsWith("https")) {
71 0 : anySsl = true;
72 : }
73 0 : } catch (URISyntaxException e) {
74 0 : System.err.println(
75 0 : String.format(
76 : "warning: invalid httpd.listenUrl entry: '%s'. Gerrit may not be able to start.",
77 : listenUrl));
78 15 : }
79 : }
80 : }
81 :
82 15 : if (listenUrls.length > 1) {
83 : // Because we will return early here, we will warn about steps otherwise being covered.
84 0 : if (!ui.isBatch()) {
85 0 : System.err.println(
86 : "Interactive configuration is not supported with multiple entries of "
87 : + "httpd.listenUrl.");
88 : }
89 0 : if (anySsl) {
90 0 : System.err.println(
91 : "Generating self-signed SSL certificates is not supported with multiple "
92 : + "entries of httpd.listenUrl.");
93 : }
94 0 : String canonicalWebUrlDefaultString = "";
95 0 : if (listenUrls[0] != null && !listenUrls[0].isEmpty()) {
96 : try {
97 0 : canonicalWebUrlDefaultString = new URI(listenUrls[0]).toString();
98 0 : } catch (URISyntaxException e) {
99 : // Should not happen, but log it anyway
100 0 : System.err.println(
101 0 : String.format("warning: invalid httpd.listenUrl entry: '%s'", listenUrls[0]));
102 0 : }
103 : }
104 0 : gerrit.string("Canonical URL", "canonicalWebUrl", canonicalWebUrlDefaultString);
105 0 : return;
106 : }
107 :
108 15 : boolean proxy = false;
109 15 : boolean ssl = false;
110 15 : String address = "*";
111 15 : int port = -1;
112 15 : String context = "/";
113 :
114 15 : if (listenUrls.length > 0 && listenUrls[0] != null && !listenUrls[0].isEmpty()) {
115 : try {
116 15 : final URI uri = toURI(listenUrls[0]);
117 15 : proxy = uri.getScheme().startsWith("proxy-");
118 15 : ssl = uri.getScheme().endsWith("https");
119 15 : address = isAnyAddress(new URI(listenUrls[0])) ? "*" : uri.getHost();
120 15 : port = uri.getPort();
121 15 : context = uri.getPath();
122 0 : } catch (URISyntaxException e) {
123 0 : System.err.println("warning: invalid httpd.listenUrl " + listenUrls[0]);
124 15 : }
125 : }
126 :
127 15 : proxy = ui.yesno(proxy, "Behind reverse proxy");
128 :
129 15 : if (proxy) {
130 1 : ssl = ui.yesno(ssl, "Proxy uses SSL (https://)");
131 1 : context = ui.readString(context, "Subdirectory on proxy server");
132 : } else {
133 15 : ssl = ui.yesno(ssl, "Use SSL (https://)");
134 15 : context = "/";
135 : }
136 :
137 15 : address = ui.readString(address, "Listen on address");
138 :
139 15 : if (port < 0) {
140 0 : if (proxy) {
141 0 : port = 8081;
142 0 : } else if (ssl) {
143 0 : port = 8443;
144 : } else {
145 0 : port = 8080;
146 : }
147 : }
148 15 : port = ui.readInt(port, "Listen on port");
149 :
150 15 : final StringBuilder urlbuf = new StringBuilder();
151 15 : urlbuf.append(proxy ? "proxy-" : "");
152 15 : urlbuf.append(ssl ? "https" : "http");
153 15 : urlbuf.append("://");
154 15 : urlbuf.append(address);
155 15 : if (0 <= port) {
156 15 : urlbuf.append(":");
157 15 : urlbuf.append(port);
158 : }
159 15 : urlbuf.append(context);
160 :
161 : URI uri;
162 : try {
163 15 : uri = toURI(urlbuf.toString());
164 15 : if (uri.getScheme().startsWith("proxy-")) {
165 : // If its a proxy URL, assume the reverse proxy is on our system
166 : // at the protocol standard ports (so omit the ports from the URL).
167 : //
168 1 : String s = uri.getScheme().substring("proxy-".length());
169 1 : uri = new URI(s + "://" + uri.getHost() + uri.getPath());
170 : }
171 0 : } catch (URISyntaxException e) {
172 0 : throw die("invalid httpd.listenUrl", e);
173 15 : }
174 15 : httpd.set("listenUrl", urlbuf.toString());
175 15 : gerrit.string("Canonical URL", "canonicalWebUrl", uri.toString());
176 15 : generateSslCertificate();
177 15 : }
178 :
179 : private void generateSslCertificate() throws IOException, InterruptedException {
180 15 : final String listenUrl = httpd.get("listenUrl");
181 :
182 15 : if (!listenUrl.startsWith("https://")) {
183 : // We aren't responsible for SSL processing.
184 : //
185 15 : return;
186 : }
187 :
188 : String hostname;
189 : try {
190 0 : String url = gerrit.get("canonicalWebUrl");
191 0 : if (url == null || url.isEmpty()) {
192 0 : url = listenUrl;
193 : }
194 0 : hostname = toURI(url).getHost();
195 0 : } catch (URISyntaxException e) {
196 0 : System.err.println("Invalid httpd.listenUrl, not checking certificate");
197 0 : return;
198 0 : }
199 :
200 0 : Path store = site.ssl_keystore;
201 0 : if (!ui.yesno(!Files.exists(store), "Create new self-signed SSL certificate")) {
202 0 : return;
203 : }
204 :
205 0 : String ssl_pass = flags.sec.get("http", null, "sslKeyPassword");
206 0 : if (ssl_pass == null || ssl_pass.isEmpty()) {
207 0 : ssl_pass = SignedToken.generateRandomKey();
208 0 : flags.sec.set("httpd", null, "sslKeyPassword", ssl_pass);
209 : }
210 :
211 0 : hostname = ui.readString(hostname, "Certificate server name");
212 0 : final String validity = ui.readString("365", "Certificate expires in (days)");
213 :
214 0 : final String dname = "CN=" + hostname + ",OU=Gerrit Code Review,O=" + domainOf(hostname);
215 :
216 0 : Path tmpdir = site.etc_dir.resolve("tmp.sslcertgen");
217 : try {
218 0 : Files.createDirectory(tmpdir);
219 0 : } catch (IOException e) {
220 0 : throw die("Cannot create directory " + tmpdir, e);
221 0 : }
222 0 : chmod(0600, tmpdir);
223 :
224 0 : Path tmpstore = tmpdir.resolve("keystore");
225 0 : Runtime.getRuntime()
226 0 : .exec(
227 : new String[] {
228 : "keytool", //
229 : "-keystore",
230 0 : tmpstore.toAbsolutePath().toString(), //
231 : "-storepass",
232 : ssl_pass, //
233 : "-genkeypair", //
234 : "-alias",
235 : hostname, //
236 : "-keyalg",
237 : "RSA", //
238 : "-validity",
239 : validity, //
240 : "-dname",
241 : dname, //
242 : "-keypass",
243 : ssl_pass, //
244 : })
245 0 : .waitFor();
246 0 : chmod(0600, tmpstore);
247 :
248 : try {
249 0 : Files.move(tmpstore, store);
250 0 : } catch (IOException e) {
251 0 : throw die("Cannot rename " + tmpstore + " to " + store, e);
252 0 : }
253 : try {
254 0 : Files.delete(tmpdir);
255 0 : } catch (IOException e) {
256 0 : throw die("Cannot delete " + tmpdir, e);
257 0 : }
258 0 : }
259 : }
|