/*
* Copyright 2014-2017 the original author or authors.
*
* 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 org.glowroot.ui;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.io.CharStreams;
import com.google.common.primitives.Ints;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.immutables.value.Value;
import org.glowroot.common.repo.ConfigRepository;
import org.glowroot.common.repo.util.Compilations;
import org.glowroot.common.repo.util.Compilations.CompilationException;
import org.glowroot.common.repo.util.Encryption;
import org.glowroot.common.repo.util.LazySecretKey;
import org.glowroot.common.repo.util.LazySecretKey.SymmetricEncryptionKeyMissingException;
import org.glowroot.common.util.ObjectMappers;
import org.glowroot.common.util.Versions;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.SyntheticMonitorConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.SyntheticMonitorConfig.SyntheticMonitorKind;
import static com.google.common.base.Preconditions.checkNotNull;
@JsonService
class SyntheticMonitorConfigJsonService {
private static final ObjectMapper mapper = ObjectMappers.create();
private static final Pattern encryptPattern = Pattern.compile("\"ENCRYPT:([^\"]*)\"");
@VisibleForTesting
static final Ordering<SyntheticMonitorConfig> orderingByDisplay =
new Ordering<SyntheticMonitorConfig>() {
@Override
public int compare(SyntheticMonitorConfig left, SyntheticMonitorConfig right) {
if (left.getKindValue() == right.getKindValue()) {
return left.getDisplay().compareToIgnoreCase(right.getDisplay());
}
return Ints.compare(left.getKindValue(), right.getKindValue());
}
};
private final ConfigRepository configRepository;
SyntheticMonitorConfigJsonService(ConfigRepository configRepository) {
this.configRepository = configRepository;
}
// central supports synthetic monitor configs on rollups
@GET(path = "/backend/config/synthetic-monitors",
permission = "agent:config:view:syntheticMonitor")
String getSyntheticMonitor(@BindAgentRollupId String agentRollupId,
@BindRequest SyntheticMonitorConfigRequest request) throws Exception {
Optional<String> id = request.id();
if (id.isPresent()) {
SyntheticMonitorConfig config =
configRepository.getSyntheticMonitorConfig(agentRollupId, id.get());
if (config == null) {
throw new JsonServiceException(HttpResponseStatus.NOT_FOUND);
}
return mapper.writeValueAsString(SyntheticMonitorConfigDto.create(config));
} else {
List<SyntheticMonitorConfigDto> configDtos = Lists.newArrayList();
List<SyntheticMonitorConfig> configs =
configRepository.getSyntheticMonitorConfigs(agentRollupId);
configs = orderingByDisplay.immutableSortedCopy(configs);
for (SyntheticMonitorConfig config : configs) {
configDtos.add(SyntheticMonitorConfigDto.create(config));
}
return mapper.writeValueAsString(configDtos);
}
}
// central supports synthetic monitor configs on rollups
@POST(path = "/backend/config/synthetic-monitors/add",
permission = "agent:config:edit:syntheticMonitor")
String addSyntheticMonitor(@BindAgentRollupId String agentRollupId,
@BindRequest SyntheticMonitorConfigDto configDto)
throws Exception {
SyntheticMonitorConfig config;
try {
config = configDto.convert(configRepository.getLazySecretKey());
} catch (SymmetricEncryptionKeyMissingException e) {
return "{\"symmetricEncryptionKeyMissing\":true}";
}
String errorResponse = validate(config);
if (errorResponse != null) {
return errorResponse;
}
String id = configRepository.insertSyntheticMonitorConfig(agentRollupId, config);
config = config.toBuilder()
.setId(id)
.build();
return mapper.writeValueAsString(SyntheticMonitorConfigDto.create(config));
}
// central supports synthetic monitor configs on rollups
@POST(path = "/backend/config/synthetic-monitors/update",
permission = "agent:config:edit:syntheticMonitor")
String updateSyntheticMonitor(@BindAgentRollupId String agentRollupId,
@BindRequest SyntheticMonitorConfigDto configDto) throws Exception {
SyntheticMonitorConfig config;
try {
config = configDto.convert(configRepository.getLazySecretKey());
} catch (SymmetricEncryptionKeyMissingException e) {
return "{\"symmetricEncryptionKeyMissing\":true}";
}
String errorResponse = validate(config);
if (errorResponse != null) {
return errorResponse;
}
configRepository.updateSyntheticMonitorConfig(agentRollupId, config,
configDto.version().get());
return mapper.writeValueAsString(SyntheticMonitorConfigDto.create(config));
}
// central supports synthetic monitor configs on rollups
@POST(path = "/backend/config/synthetic-monitors/remove",
permission = "agent:config:edit:syntheticMonitor")
void removeSyntheticMonitor(@BindAgentRollupId String agentRollupId,
@BindRequest SyntheticMonitorConfigRequest request) throws Exception {
configRepository.deleteSyntheticMonitorConfig(agentRollupId, request.id().get());
}
private @Nullable String validate(SyntheticMonitorConfig config) throws Exception {
if (config.getKind() == SyntheticMonitorKind.JAVA) {
// only used by central
try {
Class<?> javaSource = Compilations.compile(config.getJavaSource());
try {
javaSource.getConstructor();
} catch (NoSuchMethodException e) {
return buildCompilationErrorResponse(
ImmutableList.of("Class must have a public default constructor"));
}
// since synthetic monitors are only used in central, this class is present
Class<?> webDriverClass = Class.forName("org.openqa.selenium.WebDriver");
try {
javaSource.getMethod("test", new Class[] {webDriverClass});
} catch (NoSuchMethodException e) {
return buildCompilationErrorResponse(ImmutableList.of("Class must have a"
+ " \"public void test(WebDriver driver) { ... }\" method"));
}
} catch (CompilationException e) {
return buildCompilationErrorResponse(e.getCompilationErrors());
}
}
return null;
}
private String buildCompilationErrorResponse(List<String> compilationErrors)
throws IOException {
StringBuilder sb = new StringBuilder();
JsonGenerator jg = mapper.getFactory().createGenerator(CharStreams.asWriter(sb));
jg.writeStartObject();
jg.writeArrayFieldStart("javaSourceCompilationErrors");
for (String compilationError : compilationErrors) {
jg.writeString(compilationError);
}
jg.writeEndArray();
jg.writeEndObject();
jg.close();
return sb.toString();
}
@Value.Immutable
interface SyntheticMonitorConfigRequest {
Optional<String> id();
}
@Value.Immutable
abstract static class SyntheticMonitorConfigDto {
abstract String display();
abstract SyntheticMonitorKind kind();
abstract @Nullable String pingUrl();
abstract @Nullable String javaSource();
abstract Optional<String> id(); // absent for insert operations
abstract Optional<String> version(); // absent for insert operations
private SyntheticMonitorConfig convert(LazySecretKey lazySecretKey) throws Exception {
SyntheticMonitorConfig.Builder builder = SyntheticMonitorConfig.newBuilder()
.setDisplay(display())
.setKind(kind());
String pingUrl = pingUrl();
if (pingUrl != null) {
builder.setPingUrl(pingUrl);
}
String javaSource = javaSource();
if (javaSource != null) {
Matcher matcher = encryptPattern.matcher(javaSource);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String unencryptedPassword = checkNotNull(matcher.group(1));
matcher.appendReplacement(sb, "\"ENCRYPTED:"
+ Encryption.encrypt(unencryptedPassword, lazySecretKey) + "\"");
}
matcher.appendTail(sb);
builder.setJavaSource(sb.toString());
}
Optional<String> id = id();
if (id.isPresent()) {
builder.setId(id.get());
}
return builder.build();
}
private static SyntheticMonitorConfigDto create(SyntheticMonitorConfig config) {
ImmutableSyntheticMonitorConfigDto.Builder builder =
ImmutableSyntheticMonitorConfigDto.builder()
.display(config.getDisplay())
.kind(config.getKind());
String pingUrl = config.getPingUrl();
if (!pingUrl.isEmpty()) {
builder.pingUrl(pingUrl);
}
String javaSource = config.getJavaSource();
if (!javaSource.isEmpty()) {
builder.javaSource(javaSource);
}
return builder.id(config.getId())
.version(Versions.getVersion(config))
.build();
}
}
}