/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.ce.httpd;
import fi.iki.elonen.NanoHTTPD;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import org.slf4j.LoggerFactory;
import org.sonar.process.DefaultProcessCommands;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
import static fi.iki.elonen.NanoHTTPD.Response.Status.NOT_FOUND;
import static java.lang.Integer.parseInt;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
/**
* This HTTP server exports data required for display of System Info page (and the related web service).
* It listens on loopback address only, so it does not need to be secure (no HTTPS, no authentication).
*/
public class CeHttpServer {
private final Properties processProps;
private final List<HttpAction> actions;
private final ActionRegistryImpl actionRegistry;
private final CeNanoHttpd nanoHttpd;
public CeHttpServer(Properties processProps, List<HttpAction> actions) {
this.processProps = processProps;
this.actions = actions;
this.actionRegistry = new ActionRegistryImpl();
this.nanoHttpd = new CeNanoHttpd(InetAddress.getLoopbackAddress().getHostAddress(), 0, actionRegistry);
}
// do not rename. This naming convention is required for picocontainer.
public void start() {
try {
registerActions();
nanoHttpd.start();
registerHttpUrl();
} catch (IOException e) {
throw new IllegalStateException("Can not start local HTTP server for System Info monitoring", e);
}
}
private void registerActions() {
actions.forEach(action -> action.register(this.actionRegistry));
}
private void registerHttpUrl() {
int processNumber = parseInt(processProps.getProperty(PROPERTY_PROCESS_INDEX));
File shareDir = new File(processProps.getProperty(PROPERTY_SHARED_PATH));
try (DefaultProcessCommands commands = DefaultProcessCommands.secondary(shareDir, processNumber)) {
String url = getUrl();
commands.setHttpUrl(url);
LoggerFactory.getLogger(getClass()).debug("System Info HTTP server listening at {}", url);
}
}
// do not rename. This naming convention is required for picocontainer.
public void stop() {
nanoHttpd.stop();
}
// visible for testing
String getUrl() {
return "http://" + nanoHttpd.getHostname() + ":" + nanoHttpd.getListeningPort();
}
private static class CeNanoHttpd extends NanoHTTPD {
private final ActionRegistryImpl actionRegistry;
CeNanoHttpd(String hostname, int port, ActionRegistryImpl actionRegistry) {
super(hostname, port);
this.actionRegistry = actionRegistry;
}
@Override
public Response serve(IHTTPSession session) {
return actionRegistry.getAction(session)
.map(action -> serveFromAction(session, action))
.orElseGet(() -> newFixedLengthResponse(NOT_FOUND, MIME_PLAINTEXT, format("Error 404, '%s' not found.", session.getUri())));
}
private static Response serveFromAction(IHTTPSession session, HttpAction action) {
try {
return action.serve(session);
} catch (Exception e) {
return newFixedLengthResponse(INTERNAL_ERROR, MIME_PLAINTEXT, e.getMessage());
}
}
}
private static final class ActionRegistryImpl implements HttpAction.ActionRegistry {
private final Map<String, HttpAction> actionsByPath = new HashMap<>();
@Override
public void register(String path, HttpAction action) {
requireNonNull(path, "path can't be null");
requireNonNull(action, "action can't be null");
checkArgument(!path.isEmpty(), "path can't be empty");
checkArgument(!path.startsWith("/"), "path must not start with '/'");
String fixedPath = path.toLowerCase(Locale.ENGLISH);
HttpAction existingAction = actionsByPath.put(fixedPath, action);
checkState(existingAction == null, "Action '%s' already registered for path '%s'", existingAction, fixedPath);
}
Optional<HttpAction> getAction(NanoHTTPD.IHTTPSession session) {
String path = session.getUri().substring(1).toLowerCase(Locale.ENGLISH);
return Optional.ofNullable(actionsByPath.get(path));
}
}
}