All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertx.ext.httpservicefactory.HttpServiceFactory Maven / Gradle / Ivy

There is a newer version: 4.5.11
Show newest version
package io.vertx.ext.httpservicefactory;

import io.vertx.core.*;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.AsyncFile;
import io.vertx.core.file.OpenOptions;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.RequestOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
import io.vertx.service.ServiceVerticleFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;

import java.io.File;
import java.io.FileInputStream;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * @author Julien Viet
 */
public class HttpServiceFactory extends ServiceVerticleFactory {

  public static final String CACHE_DIR_PROPERTY = "vertx.httpServiceFactory.cacheDir";
  public static final String HTTP_CLIENT_OPTIONS_PROPERTY = "vertx.httpServiceFactory.httpClientOptions";
  public static final String HTTPS_CLIENT_OPTIONS_PROPERTY = "vertx.httpServiceFactory.httpsClientOptions";
  public static final String AUTH_USERNAME_PROPERTY = "vertx.httpServiceFactory.authUsername";
  public static final String AUTH_PASSWORD_PROPERTY = "vertx.httpServiceFactory.authPassword";
  public static final String PROXY_HOST_PROPERTY = "vertx.httpServiceFactory.proxyHost";
  public static final String PROXY_PORT_PROPERTY = "vertx.httpServiceFactory.proxyPort";
  public static final String KEYSERVER_URI_TEMPLATE = "vertx.httpServiceFactory.keyserverURITemplate";
  public static final String VALIDATION_POLICY = "vertx.httpServiceFactory.validationPolicy";

  private static final String FILE_SEP = System.getProperty("file.separator");
  private static final String FILE_CACHE_DIR = ".vertx" + FILE_SEP + "vertx-http-service-factory";

  private Vertx vertx;
  private File cacheDir;
  private String username;
  private String password;
  private String keyserverURITemplate;
  private ValidationPolicy validationPolicy;
  private HttpClientOptions options;

  @Override
  public void init(Vertx vertx) {
    cacheDir = new File(System.getProperty(CACHE_DIR_PROPERTY, FILE_CACHE_DIR));
    validationPolicy = ValidationPolicy.valueOf(System.getProperty(VALIDATION_POLICY, ValidationPolicy.VERIFY.toString()).toUpperCase());
    username = System.getProperty(AUTH_USERNAME_PROPERTY);
    password = System.getProperty(AUTH_PASSWORD_PROPERTY);
    options = configOptions();
    keyserverURITemplate = System.getProperty(KEYSERVER_URI_TEMPLATE, "http://pool.sks-keyservers.net:11371/pks/lookup?op=get&options=mr&search=0x%016X");
    this.vertx = vertx;
  }

  protected HttpClientOptions createHttpClientOptions(String scheme) {
    HttpClientOptions options;
    if ("https".equals(scheme)) {
      String optionsJson = System.getProperty(HTTPS_CLIENT_OPTIONS_PROPERTY);
      if (optionsJson != null) {
        options = new HttpClientOptions(new JsonObject(optionsJson));
      } else {
        options = createHttpClientOptions("http").setTrustAll(true);
      }
      options.setSsl(true);
    } else {
      String optionsJson = System.getProperty(HTTP_CLIENT_OPTIONS_PROPERTY);
      options = optionsJson != null ? new HttpClientOptions(new JsonObject(optionsJson)) : new HttpClientOptions();
    }
    String proxyHost = System.getProperty(PROXY_HOST_PROPERTY);
    int proxyPort = Integer.parseInt(System.getProperty(PROXY_PORT_PROPERTY, "-1"));
    if (proxyHost != null) {
      ProxyOptions proxyOptions = new ProxyOptions().setHost(proxyHost).setType(ProxyType.HTTP);
      if (proxyPort > 0) {
        proxyOptions.setPort(proxyPort);
      }
      options.setProxyOptions(proxyOptions);
    }
    return options;
  }

  protected HttpClientOptions configOptions() {
    return createHttpClientOptions(prefix());
  }

  @Override
  public String prefix() {
    return "http";
  }

  @Override
  protected void createVerticle(String verticleName, DeploymentOptions deploymentOptions, ClassLoader classLoader, Promise> promise) {



    int pos = verticleName.lastIndexOf("::");
    String serviceName;
    String stringURL;
    if (pos != -1) {
      stringURL = verticleName.substring(0, pos);
      serviceName = verticleName.substring(pos + 2);
    } else {
      serviceName = null;
      stringURL = verticleName;
    }

    URI url;
    URI signatureURL;
    String deploymentKey;
    String signatureKey;
    try {
      url = new URI(stringURL);
      signatureURL = new URI(url.getScheme(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath() + ".asc", url.getQuery(), url.getFragment());
      deploymentKey = URLEncoder.encode(url.toString(), "UTF-8");
      signatureKey = URLEncoder.encode(signatureURL.toString(), "UTF-8");
    } catch (Exception e) {
      promise.fail(e);
      return;
    }
    File deploymentFile = new File(cacheDir, deploymentKey);
    File signatureFile = new File(cacheDir, signatureKey);

    //
    HttpClient client = vertx.createHttpClient(options);
    doRequest(client, deploymentFile, url, signatureFile, signatureURL, ar -> {
      if (ar.succeeded()) {
        if (ar.result().signature != null) {
          PGPSignature signature;
          URI publicKeyURI;
          File publicKeyFile;
          try {
            signature = PGPHelper.getSignature(Files.readAllBytes(ar.result().signature.toPath()));
            String uri = String.format(keyserverURITemplate, signature.getKeyID());
            publicKeyURI = new URI(uri);
            publicKeyFile = new File(cacheDir, URLEncoder.encode(publicKeyURI.toString(), "UTF-8"));
          } catch (Exception e) {
            closeQuietly(client);
            promise.fail(e);
            return;
          }
          HttpClient keyserverClient;
          if (!publicKeyURI.getScheme().equals(prefix())) {
            closeQuietly(client);
            keyserverClient = vertx.createHttpClient(createHttpClientOptions(publicKeyURI.getScheme()));
          } else {
            keyserverClient = client;
          }

          BiFunction unmarshallerFactory = (mediaType, buf) -> {
            switch (mediaType) {
              case "application/json":
                JsonObject json = new JsonObject(buf.toString());
                return Buffer.buffer(json.getJsonArray("keys").getJsonObject(0).getString("bundle"));
              case "application/pgp-keys":
              default:
                return buf;
            }
          };

          doRequest(keyserverClient, publicKeyFile, publicKeyURI, null, null, false, unmarshallerFactory, ar2 -> {
            if (ar2.succeeded()) {
              try {
                long keyID = signature.getKeyID();
                File file = ar2.result();
                Path path = file.toPath();
                PGPPublicKey publicKey = PGPHelper.getPublicKey(Files.readAllBytes(path), keyID);
                if (publicKey != null) {
                  FileInputStream f = new FileInputStream(ar.result().deployment);
                  boolean verified = PGPHelper.verifySignature(f, new FileInputStream(ar.result().signature), publicKey);
                  if (verified) {
                    deploy(deploymentFile, verticleName, serviceName, deploymentOptions, classLoader, promise);
                    return;
                  }
                }
                promise.fail(new Exception("Signature verification failed"));
              } catch (Exception e) {
                promise.fail(e);
              } finally {
                closeQuietly(keyserverClient);
              }
            } else {
              closeQuietly(keyserverClient);
              promise.fail(ar2.cause());
            }
          });
        } else {
          closeQuietly(client);
          deploy(deploymentFile, verticleName, serviceName, deploymentOptions, classLoader, promise);
        }
      } else {
        promise.fail(ar.cause());
      }
    });
  }

  /**
   * The {@code unmarshallerFactory} argument is a function that returns an {@code Function} unmarshaller
   * function for a given media type value. The returned function unmarshaller function will be called with the buffers
   * to unmarshall and finally with a null buffer to signal the end of the unmarshalled data. It can return a buffer
   * or a null value.
   *  @param client       the http client
   * @param file         the file where to save the content
   * @param url          the resource url
   * @param username     the optional username used for basic auth
   * @param password     the optional password used for basic auth
   * @param doAuth       whether to perform authentication or not
   * @param unmarshaller the unmarshaller
   * @param handler      the result handler
   */
  private void doRequest(
    HttpClient client,
    File file,
    URI url,
    String username,
    String password,
    boolean doAuth,
    BiFunction unmarshaller,
    Handler> handler) {
    if (file.exists() && file.isFile()) {
      handler.handle(Future.succeededFuture(file));
      return;
    }
    String requestURI = url.getPath();
    if (url.getQuery() != null) {
      requestURI += "?" + url.getQuery();
    }
    int port = url.getPort();
    if (port == -1) {
      if ("http".equals(url.getScheme())) {
        port = 80;
      } else {
        port = 443;
      }
    }
    RequestOptions options = new RequestOptions()
      .setPort(port)
      .setHost(url.getHost())
      .setURI(requestURI)
      .setFollowRedirects(true)
      .addHeader("user-agent", "Vert.x Http Service Factory");
    if (doAuth && username != null && password != null) {
      options.addHeader("Authorization", "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()));
    }
    client.request(options, ar1 -> {
      if (ar1.succeeded()) {
        HttpClientRequest req = ar1.result();
        req.send(ar2 -> {
          if (ar2.succeeded()) {
            HttpClientResponse resp = ar2.result();
            int status = resp.statusCode();
            switch (resp.statusCode()) {
              case 200: {
                String contentType = resp.getHeader("Content-Type");
                int index = contentType.indexOf(";");
                String mediaType = index > -1 ? contentType.substring(0, index) : contentType;
                AtomicBoolean done = new AtomicBoolean();
                resp.exceptionHandler(err -> {
                  if (done.compareAndSet(false, true)) {
                    handler.handle(Future.failedFuture(err));
                  }
                });
                resp.bodyHandler(body -> {
                  if (!done.compareAndSet(false, true)) {
                    return;
                  }
                  File parentFile = file.getParentFile();
                  if (!parentFile.exists()) {
                    parentFile.mkdirs(); // Handle that
                  }
                  Buffer data;
                  try {
                    data = unmarshaller.apply(mediaType, body);
                  } catch (Exception e) {
                    handler.handle(Future.failedFuture(e));
                    return;
                  }
                  vertx.fileSystem().open(file.getPath(), new OpenOptions().setCreate(true), ar3 -> {
                    if (ar3.succeeded()) {
                      AsyncFile result = ar3.result();
                      result.write(data);
                      result.close(v2 -> {
                        if (v2.succeeded()) {
                          handler.handle(Future.succeededFuture(file));
                        } else {
                          handler.handle(Future.failedFuture(v2.cause()));
                        }
                      });
                    } else {
                      handler.handle(Future.failedFuture(ar3.cause()));
                    }
                  });
                });
                break;
              }
              case 401: {
                if (prefix().equals("https") && resp.getHeader("WWW-Authenticate") != null && username != null && password != null) {
                  doRequest(client, file, url, username, password, true, unmarshaller, handler);
                  return;
                }
                handler.handle(Future.failedFuture(new Exception("Unauthorized")));
                break;
              }
              default: {
                handler.handle(Future.failedFuture(new Exception("Cannot get file status:" + status)));
                break;
              }
            }
          } else {
            handler.handle(Future.failedFuture(ar2.cause()));
          }
        });
      } else {
        handler.handle(Future.failedFuture(ar1.cause()));
      }
    });
  }

  private static class Result {

    final File deployment;
    final File signature;

    public Result(File deployment, File signature) {
      this.deployment = deployment;
      this.signature = signature;
    }
  }

  protected void doRequest(HttpClient client, File file, URI url, File signatureFile,
                           URI signatureURL, Handler> handler) {
    doRequest(client, file, url, username, password, false, (mediatype, buf) -> buf, ar1 -> {
      if (ar1.succeeded()) {
        // Now get the signature if any
        if (validationPolicy != ValidationPolicy.NONE) {
          doRequest(client, signatureFile, signatureURL, username, password, false, (mediatype, buf) -> buf, ar3 -> {
            if (ar3.succeeded()) {
              handler.handle(Future.succeededFuture(new Result(ar1.result(), ar3.result())));
            } else {
              if (validationPolicy == ValidationPolicy.MANDATORY) {
                handler.handle(Future.failedFuture(ar3.cause()));
              } else {
                handler.handle(Future.succeededFuture(new Result(ar1.result(), null)));
              }
            }
          });
        } else {
          handler.handle(Future.succeededFuture(new Result(file, null)));
        }
      } else {
        handler.handle(Future.failedFuture(ar1.cause()));
      }
    });
  }

  private void deploy(File file, String identifier, String serviceName, DeploymentOptions deploymentOptions, ClassLoader classLoader, Promise> resolution) {
    try {
      String serviceIdentifer = null;
      if (serviceName == null) {
        JarFile jarFile = new JarFile(file);
        Manifest manifest = jarFile.getManifest();
        if (manifest != null) {
          serviceIdentifer = (String) manifest.getMainAttributes().get(new Attributes.Name("Main-Verticle"));
        }
      } else {
        serviceIdentifer = "service:" + serviceName;
      }
      if (serviceIdentifer == null) {
        throw new IllegalArgumentException("Invalid service identifier, missing service name: " + identifier);
      }
      deploymentOptions.setExtraClasspath(Collections.singletonList(file.getAbsolutePath()));
      deploymentOptions.setIsolationGroup("__vertx_maven_" + file.getName());
      URLClassLoader urlc = new URLClassLoader(new URL[]{file.toURI().toURL()}, classLoader);
      super.createVerticle(serviceIdentifer, deploymentOptions, urlc, resolution);
    } catch (Exception e) {
      resolution.fail(e);
    }
  }

  private void closeQuietly(HttpClient client) {
    try {
      client.close();
    } catch (Exception e) {
      // We ignore the exceptions.
      // If the client was already closed, it throws an IllegalStateException.
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy