/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.discovery.shared.transport;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.InstanceInfo.InstanceStatus;
import com.netflix.discovery.converters.wrappers.CodecWrappers;
import com.netflix.discovery.converters.wrappers.CodecWrappers.JacksonJson;
import com.netflix.discovery.converters.wrappers.DecoderWrapper;
import com.netflix.discovery.converters.wrappers.EncoderWrapper;
import com.netflix.discovery.shared.Applications;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* HTTP server with Eureka compatible REST API that delegates client request to the provided {@link EurekaHttpClient}
* implementation. It is very lightweight implementation that can be used in unit test without incurring to much
* overhead.
*
* @author Tomasz Bak
*/
public class SimpleEurekaHttpServer {
private static final Logger logger = LoggerFactory.getLogger(SimpleEurekaHttpServer.class);
private final EurekaHttpClient requestHandler;
private final EurekaTransportEventListener eventListener;
private final HttpServer httpServer;
private final EncoderWrapper encoder = CodecWrappers.getEncoder(JacksonJson.class);
private final DecoderWrapper decoder = CodecWrappers.getDecoder(JacksonJson.class);
public SimpleEurekaHttpServer(EurekaHttpClient requestHandler) throws IOException {
this(requestHandler, null);
}
public SimpleEurekaHttpServer(EurekaHttpClient requestHandler, EurekaTransportEventListener eventListener) throws IOException {
this.requestHandler = requestHandler;
this.eventListener = eventListener;
this.httpServer = HttpServer.create(new InetSocketAddress(0), 1);
httpServer.createContext("/v2", createEurekaV2Handle());
httpServer.setExecutor(null);
httpServer.start();
}
public void shutdown() {
httpServer.stop(0);
}
public URI getServiceURI() {
try {
return new URI("http://localhost:" + getServerPort() + "/v2/");
} catch (URISyntaxException e) {
throw new IllegalStateException("Cannot parse service URI", e);
}
}
public int getServerPort() {
return httpServer.getAddress().getPort();
}
private HttpHandler createEurekaV2Handle() {
return new HttpHandler() {
@Override
public void handle(HttpExchange httpExchange) throws IOException {
if(eventListener != null) {
eventListener.onHttpRequest(mapToEurekaHttpRequest(httpExchange));
}
try {
String method = httpExchange.getRequestMethod();
String path = httpExchange.getRequestURI().getPath();
if (path.startsWith("/v2/apps")) {
if ("GET".equals(method)) {
handleAppsGET(httpExchange);
} else if ("POST".equals(method)) {
handleAppsPost(httpExchange);
} else if ("PUT".equals(method)) {
handleAppsPut(httpExchange);
} else if ("DELETE".equals(method)) {
handleAppsDelete(httpExchange);
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
}
} else if (path.startsWith("/v2/vips")) {
handleVipsGET(httpExchange);
} else if (path.startsWith("/v2/svips")) {
handleSecureVipsGET(httpExchange);
} else if (path.startsWith("/v2/instances")) {
handleInstanceGET(httpExchange);
}
} catch (Exception e) {
logger.error("HttpServer error", e);
httpExchange.sendResponseHeaders(500, 0);
}
httpExchange.close();
}
};
}
private void handleAppsGET(HttpExchange httpExchange) throws IOException {
EurekaHttpResponse<?> httpResponse;
String path = httpExchange.getRequestURI().getPath();
Matcher matcher;
if (path.matches("/v2/apps[/]?")) {
String regions = getQueryParam(httpExchange, "regions");
httpResponse = regions == null ? requestHandler.getApplications() : requestHandler.getApplications(regions);
} else if (path.matches("/v2/apps/delta[/]?")) {
String regions = getQueryParam(httpExchange, "regions");
httpResponse = regions == null ? requestHandler.getDelta() : requestHandler.getDelta(regions);
} else if ((matcher = Pattern.compile("/v2/apps/([^/]+)/([^/]+)").matcher(path)).matches()) {
httpResponse = requestHandler.getInstance(matcher.group(1), matcher.group(2));
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
return;
}
if (httpResponse == null) {
httpResponse = EurekaHttpResponse.anEurekaHttpResponse(HttpServletResponse.SC_NOT_FOUND).build();
}
mapResponse(httpExchange, httpResponse);
}
private void handleAppsPost(HttpExchange httpExchange) throws IOException {
EurekaHttpResponse<?> httpResponse;
String path = httpExchange.getRequestURI().getPath();
if (path.matches("/v2/apps/([^/]+)(/)?")) {
InstanceInfo instance = decoder.decode(httpExchange.getRequestBody(), InstanceInfo.class);
httpResponse = requestHandler.register(instance);
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
return;
}
mapResponse(httpExchange, httpResponse);
}
private void handleAppsPut(HttpExchange httpExchange) throws IOException {
EurekaHttpResponse<?> httpResponse;
String path = httpExchange.getRequestURI().getPath();
Matcher matcher;
if ((matcher = Pattern.compile("/v2/apps/([^/]+)/([^/]+)").matcher(path)).matches()) {
String overriddenstatus = getQueryParam(httpExchange, "overriddenstatus");
httpResponse = requestHandler.sendHeartBeat(
matcher.group(1), matcher.group(2), null,
overriddenstatus == null ? null : InstanceStatus.valueOf(overriddenstatus)
);
} else if ((matcher = Pattern.compile("/v2/apps/([^/]+)/([^/]+)/status").matcher(path)).matches()) {
String newStatus = getQueryParam(httpExchange, "value");
httpResponse = requestHandler.statusUpdate(
matcher.group(1), matcher.group(2),
newStatus == null ? null : InstanceStatus.valueOf(newStatus),
null
);
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
return;
}
mapResponse(httpExchange, httpResponse);
}
private void handleAppsDelete(HttpExchange httpExchange) throws IOException {
EurekaHttpResponse<?> httpResponse;
String path = httpExchange.getRequestURI().getPath();
Matcher matcher;
if ((matcher = Pattern.compile("/v2/apps/([^/]+)/([^/]+)").matcher(path)).matches()) {
httpResponse = requestHandler.cancel(matcher.group(1), matcher.group(2));
} else if ((matcher = Pattern.compile("/v2/apps/([^/]+)/([^/]+)/status").matcher(path)).matches()) {
httpResponse = requestHandler.deleteStatusOverride(matcher.group(1), matcher.group(2), null);
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
return;
}
mapResponse(httpExchange, httpResponse);
}
private void handleVipsGET(HttpExchange httpExchange) throws IOException {
Matcher matcher = Pattern.compile("/v2/vips/([^/]+)").matcher(httpExchange.getRequestURI().getPath());
if (matcher.matches()) {
String regions = getQueryParam(httpExchange, "regions");
EurekaHttpResponse<Applications> httpResponse = regions == null
? requestHandler.getVip(matcher.group(1))
: requestHandler.getVip(matcher.group(1), regions);
mapResponse(httpExchange, httpResponse);
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
}
}
private void handleSecureVipsGET(HttpExchange httpExchange) throws IOException {
Matcher matcher = Pattern.compile("/v2/svips/([^/]+)").matcher(httpExchange.getRequestURI().getPath());
if (matcher.matches()) {
String regions = getQueryParam(httpExchange, "regions");
EurekaHttpResponse<Applications> httpResponse = regions == null
? requestHandler.getSecureVip(matcher.group(1))
: requestHandler.getSecureVip(matcher.group(1), regions);
mapResponse(httpExchange, httpResponse);
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
}
}
private void handleInstanceGET(HttpExchange httpExchange) throws IOException {
Matcher matcher = Pattern.compile("/v2/instances/([^/]+)").matcher(httpExchange.getRequestURI().getPath());
if (matcher.matches()) {
mapResponse(httpExchange, requestHandler.getInstance(matcher.group(1)));
} else {
httpExchange.sendResponseHeaders(HttpServletResponse.SC_NOT_FOUND, 0);
}
}
private EurekaHttpRequest mapToEurekaHttpRequest(HttpExchange httpExchange) {
Headers exchangeHeaders = httpExchange.getRequestHeaders();
Map<String, String> headers = new HashMap<>();
for(String key: exchangeHeaders.keySet()) {
headers.put(key, exchangeHeaders.getFirst(key));
}
return new EurekaHttpRequest(httpExchange.getRequestMethod(), httpExchange.getRequestURI(), headers);
}
private <T> void mapResponse(HttpExchange httpExchange, EurekaHttpResponse<T> response) throws IOException {
// Add headers
for (Map.Entry<String, String> headerEntry : response.getHeaders().entrySet()) {
httpExchange.getResponseHeaders().add(headerEntry.getKey(), headerEntry.getValue());
}
if (response.getStatusCode() / 100 != 2) {
httpExchange.sendResponseHeaders(response.getStatusCode(), 0);
return;
}
// Prepare body, if any
T entity = response.getEntity();
byte[] body = null;
if (entity != null) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
encoder.encode(entity, bos);
body = bos.toByteArray();
}
// Set status and body length
httpExchange.sendResponseHeaders(response.getStatusCode(), body == null ? 0 : body.length);
// Send body
if (body != null) {
OutputStream responseStream = httpExchange.getResponseBody();
try {
responseStream.write(body);
responseStream.flush();
} finally {
responseStream.close();
}
}
}
private static String getQueryParam(HttpExchange httpExchange, String queryParam) {
String query = httpExchange.getRequestURI().getQuery();
if (query != null) {
for (String part : query.split("&")) {
String[] keyValue = part.split("=");
if (keyValue.length > 1 && keyValue[0].equals(queryParam)) {
return keyValue[1];
}
}
}
return null;
}
}