package io.divolte.server; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.google.common.collect.ImmutableList; import com.typesafe.config.ConfigFactory; import io.divolte.server.config.ValidatedConfiguration; import io.divolte.server.ip2geo.ExternalDatabaseLookupService; import io.divolte.server.ip2geo.LookupService; import io.divolte.server.recordmapping.DslRecordMapper; import io.undertow.Undertow; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.Headers; import io.undertow.util.Methods; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import org.apache.avro.Schema; import org.apache.avro.Schema.Parser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnio.streams.ChannelInputStream; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.time.Instant; import java.util.Optional; import static io.divolte.server.IncomingRequestProcessor.DUPLICATE_EVENT_KEY; @ParametersAreNonnullByDefault public class MappingTestServer { private static final Logger log = LoggerFactory.getLogger(MappingTestServer.class); private static final ObjectReader EVENT_PARAMETERS_READER = new ObjectMapper().reader(); private final DslRecordMapper mapper; private final Undertow undertow; public static void main(final String[] args) throws IOException { final MappingTestServerOptionParser parser = new MappingTestServerOptionParser(); try { final OptionSet options = parser.parse(args); if (options.has("help")) { parser.printHelpOn(System.out); } else { final String host = options.valueOf(parser.hostOption); final Integer port = options.valueOf(parser.portOption); final MappingTestServer server = new MappingTestServer(options.valueOf(parser.schemaOption), options.valueOf(parser.mappingOption), port, host); Runtime.getRuntime().addShutdownHook(new Thread(server::stopServer)); log.info("Starting server on {}:{}...", host, port); server.runServer(); } } catch (final OptionException e) { System.err.println(e.getLocalizedMessage()); parser.printHelpOn(System.err); } } public MappingTestServer(final String schemaFilename, final String mappingFilename, final int port, final String host) throws IOException { final Schema schema = loadSchema(schemaFilename); final ValidatedConfiguration vc = new ValidatedConfiguration(ConfigFactory::load); mapper = new DslRecordMapper(vc, mappingFilename, schema, Optional.ofNullable(lookupServiceFromConfig(vc))); final HttpHandler handler = new AllowedMethodsHandler(this::handleEvent, Methods.POST); final HttpHandler rootHandler = new ProxyAdjacentPeerAddressHandler(handler); undertow = Undertow.builder() .addHttpListener(port, host) .setHandler(rootHandler) .build(); } private Schema loadSchema(final String schemaFilename) throws IOException { final Parser parser = new Schema.Parser(); return parser.parse(new File(schemaFilename)); } @Nullable private static LookupService lookupServiceFromConfig(final ValidatedConfiguration vc) { return vc.configuration().global.mapper.ip2geoDatabase .map((path) -> { try { return new ExternalDatabaseLookupService(Paths.get(path)); } catch (final IOException e) { throw new RuntimeException("Failed to configure GeoIP lookup service.", e); } }).orElse(null); } private void runServer() { undertow.start(); } private void stopServer() { undertow.stop(); } private void handleEvent(final HttpServerExchange exchange) throws Exception { try (final ChannelInputStream cis = new ChannelInputStream(exchange.getRequestChannel())) { final JsonNode payload = EVENT_PARAMETERS_READER.readTree(cis); final String generatedPageViewId = DivolteIdentifier.generate().value; final DivolteEvent.BrowserEventData browserEventData = new DivolteEvent.BrowserEventData( get(payload, "page_view_id", String.class).orElse(generatedPageViewId), get(payload, "location", String.class), get(payload, "referer", String.class), get(payload, "viewport_pixel_width", Integer.class), get(payload, "viewport_pixel_height", Integer.class), get(payload, "screen_pixel_width", Integer.class), get(payload, "screen_pixel_height", Integer.class), get(payload, "device_pixel_ratio", Integer.class)); final Instant now = Instant.now(); final DivolteEvent divolteEvent = DivolteEvent.createBrowserEvent( exchange, get(payload, "corrupt", Boolean.class).orElse(false), get(payload, "party_id", String.class).flatMap(DivolteIdentifier::tryParse).orElse(DivolteIdentifier.generate()), get(payload, "session_id", String.class).flatMap(DivolteIdentifier::tryParse).orElse(DivolteIdentifier.generate()), get(payload, "event_id", String.class).orElse(generatedPageViewId + "0"), now, now, get(payload, "new_party_id", Boolean.class).orElse(false), get(payload, "first_in_session", Boolean.class).orElse(false), get(payload, "event_type", String.class), () -> get(payload, "parameters", JsonNode.class), browserEventData); get(payload, "remote_host", String.class) .ifPresent(ip -> { try { final InetAddress address = InetAddress.getByName(ip); // We have no way of knowing the port exchange.setSourceAddress(new InetSocketAddress(address, 0)); } catch (final UnknownHostException e) { log.warn("Could not parse remote host: " + ip, e); } }); exchange.putAttachment(DUPLICATE_EVENT_KEY, get(payload, "duplicate", Boolean.class).orElse(false)); exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json"); exchange.getResponseChannel().write(ByteBuffer.wrap(mapper.newRecordFromExchange(divolteEvent).toString().getBytes(StandardCharsets.UTF_8))); exchange.endExchange(); } } private static <T> Optional<T> get(final JsonNode jsonResult, final String key, final Class<T> type) { final Optional<JsonNode> fieldNode = Optional.ofNullable(jsonResult.get(key)); return fieldNode.map(f -> { try { return EVENT_PARAMETERS_READER.treeToValue(f, type); } catch (final JsonProcessingException e) { log.info("Unable to map field: " + key, e); return null; } }); } private static class MappingTestServerOptionParser extends OptionParser { final ArgumentAcceptingOptionSpec<String> schemaOption = acceptsAll(ImmutableList.of("schema", "s"), "The AVRO schema of the records to generate.") .withRequiredArg().required(); final ArgumentAcceptingOptionSpec<String> mappingOption = acceptsAll(ImmutableList.of("mapping", "m"), "The mapping definition to use for processing requests.") .withRequiredArg().required(); final ArgumentAcceptingOptionSpec<String> hostOption = acceptsAll(ImmutableList.of("host", "i"), "The address of the interface to listen on.") .withRequiredArg().defaultsTo("localhost"); final ArgumentAcceptingOptionSpec<Integer> portOption = acceptsAll(ImmutableList.of("port", "p"), "The TCP port to listen on.") .withRequiredArg().ofType(Integer.class).defaultsTo(8390); public MappingTestServerOptionParser() { acceptsAll(ImmutableList.of("help", "h"), "Display this help.").forHelp(); } } }