From b459fd96188b796deaac70a80584d419c3c1964c Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 15 Oct 2016 13:11:38 -0300 Subject: [PATCH 1/4] crash: remote shell module fix #499 --- .../main/java/org/jooby/banner/Banner.java | 17 +- jooby-crash/pom.xml | 183 ++++++++++ .../src/main/java/org/jooby/crash/Crash.java | 328 ++++++++++++++++++ .../java/org/jooby/crash/CrashBootstrap.java | 109 ++++++ .../java/org/jooby/crash/CrashFSDriver.java | 207 +++++++++++ .../java/org/jooby/crash/HttpShellPlugin.java | 59 ++++ .../org/jooby/crash/SimpleProcessContext.java | 135 +++++++ .../java/org/jooby/crash/WebShellHandler.java | 142 ++++++++ .../java/org/jooby/crash/WebShellPlugin.java | 84 +++++ .../resources/org/jooby/crash/conf.groovy | 78 +++++ .../main/resources/org/jooby/crash/crash.conf | 2 + .../resources/org/jooby/crash/login.groovy | 19 + .../resources/org/jooby/crash/routes.groovy | 43 +++ jooby-crash/src/test/java/app/AuthPlugin.java | 35 ++ jooby-crash/src/test/java/app/CrashApp.java | 38 ++ jooby-crash/src/test/resources/crash.conf | 8 + .../assemblies/jooby.flexible.zip.xml | 10 + .../main/resources/assemblies/jooby.stork.xml | 21 +- .../main/resources/assemblies/jooby.war.xml | 11 + .../main/resources/assemblies/jooby.zip.xml | 10 + md/doc/crash/crash.md | 135 +++++++ md/doc/deployment/deployment.md | 2 + md/doc/more/more.md | 3 +- pom.xml | 85 +++++ 24 files changed, 1751 insertions(+), 13 deletions(-) create mode 100644 jooby-crash/pom.xml create mode 100644 jooby-crash/src/main/java/org/jooby/crash/Crash.java create mode 100644 jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java create mode 100644 jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java create mode 100644 jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java create mode 100644 jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java create mode 100644 jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java create mode 100644 jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java create mode 100644 jooby-crash/src/main/resources/org/jooby/crash/conf.groovy create mode 100644 jooby-crash/src/main/resources/org/jooby/crash/crash.conf create mode 100644 jooby-crash/src/main/resources/org/jooby/crash/login.groovy create mode 100644 jooby-crash/src/main/resources/org/jooby/crash/routes.groovy create mode 100644 jooby-crash/src/test/java/app/AuthPlugin.java create mode 100644 jooby-crash/src/test/java/app/CrashApp.java create mode 100644 jooby-crash/src/test/resources/crash.conf create mode 100644 md/doc/crash/crash.md diff --git a/jooby-banner/src/main/java/org/jooby/banner/Banner.java b/jooby-banner/src/main/java/org/jooby/banner/Banner.java index 249a231508..c27534abd5 100644 --- a/jooby-banner/src/main/java/org/jooby/banner/Banner.java +++ b/jooby-banner/src/main/java/org/jooby/banner/Banner.java @@ -23,14 +23,20 @@ import java.util.Optional; +import javax.inject.Provider; + import org.jooby.Env; import org.jooby.Jooby.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.name.Names; import com.typesafe.config.Config; +import javaslang.control.Try; + /** *

banner

*

@@ -105,8 +111,15 @@ public void configure(final Env env, final Config conf, final Binder binder) { String v = conf.getString("application.version"); String text = this.text.orElse(name); - env.onStart( - () -> log.info("\n{} v{}\n", convertOneLine(String.format(FONT, font), text).trim(), v)); + Provider ascii = () -> Try + .of(() -> convertOneLine(String.format(FONT, font), text).trim()) + .getOrElse(text); + + binder.bind(Key.get(String.class, Names.named("application.banner"))).toProvider(ascii); + + env.onStart(() -> { + log.info("\n{} v{}\n", ascii.get(), v); + }); } public Banner font(final String font) { diff --git a/jooby-crash/pom.xml b/jooby-crash/pom.xml new file mode 100644 index 0000000000..10ba135920 --- /dev/null +++ b/jooby-crash/pom.xml @@ -0,0 +1,183 @@ + + + + + org.jooby + jooby-project + 1.0.0-SNAPSHOT + + + 4.0.0 + jooby-crash + + crash shell module + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + **/*Feature.java + **/Issue*.java + + + + + + org.basepom.maven + duplicate-finder-maven-plugin + 1.2.1 + + + duplicate-dependencies + validate + + check + + + + crash/crash.properties + + + + + + + + + + + + + org.jooby + jooby + ${project.version} + + + + org.slf4j + jul-to-slf4j + ${slf4j-api.version} + + + + org.slf4j + log4j-over-slf4j + ${slf4j-api.version} + + + + org.slf4j + jcl-over-slf4j + ${slf4j-api.version} + + + + org.codehaus.groovy + groovy + + + + org.crashub + crash.shell + + + + + org.jooby + jooby + ${project.version} + test + tests + + + + junit + junit + test + + + + org.easymock + easymock + test + + + + org.powermock + powermock-api-easymock + test + + + + org.powermock + powermock-module-junit4 + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + org.jooby + jooby-netty + ${project.version} + test + + + + org.jooby + jooby-jackson + ${project.version} + test + + + + org.jooby + jooby-banner + ${project.version} + test + + + + org.crashub + crash.connectors.telnet + test + + + + org.crashub + crash.connectors.web + test + + + + org.crashub + crash.connectors.ssh + test + + + + org.crashub + crash.plugins.mail + test + + + + org.crashub + crash.plugins.cron + test + + + + + diff --git a/jooby-crash/src/main/java/org/jooby/crash/Crash.java b/jooby-crash/src/main/java/org/jooby/crash/Crash.java new file mode 100644 index 0000000000..43fd4df154 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/Crash.java @@ -0,0 +1,328 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.Set; + +import org.crsh.plugin.CRaSHPlugin; +import org.crsh.plugin.PluginContext; +import org.jooby.Env; +import org.jooby.Jooby.Module; +import org.jooby.Registry; +import org.jooby.Route; +import org.jooby.WebSocket; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import com.google.common.collect.Sets; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +import javaslang.concurrent.Promise; + +/** + *

crash

+ *

+ * Connect, monitor or use virtual machine resources via SSH, telnet or HTTP with + * CRaSH remote shell. + *

+ * + *

usage

+ * + *
{@code
+ * import org.jooby.crash;
+ *
+ * {
+ *   use(new Crash());
+ * }
+ * }
+ * + *

+ * That's all you need to get CRaSH up and running!!! + *

+ *

+ * Now is time to see how to connect and interact with the CRaSH + * shell + *

+ * + *

commands

+ *

+ * You can write additional shell commands using Groovy or Java, see the + * CRaSH documentation for + * details. CRaSH search for commands in the cmd folder. + *

+ *

+ * Here is a simple ‘hello’ command that could be loaded from cmd/hello.groovy folder: + *

+ *
{@code
+ * package commands
+ *
+ * import org.crsh.cli.Command
+ * import org.crsh.cli.Usage
+ * import org.crsh.command.InvocationContext
+ *
+ * class hello {
+ *
+ *  @Usage("Say Hello")
+ *  @Command
+ *  def main(InvocationContext context) {
+ *      return "Hello"
+ *  }
+ *
+ * }
+ * }
+ * + *

+ * Jooby adds some additional attributes and commands to InvocationContext that you can access from + * your command: + *

+ * + * + * + *

routes command

+ *

+ * The routes print all the application routes. + *

+ * + *

conf command

+ *

+ * The conf tree print the application configuration tree (configuration precedence). + *

+ * + *

+ * The conf props [path] print all the application properties, sub-tree or a single + * property if path argument is present. + *

+ * + *

connectors

+ * + *

HTTP connector

+ *

+ * The HTTP connector is a simple yet powerful collection of HTTP endpoints where you can + * run + * CRaSH + * command: + *

+ * + *
{@code
+ *
+ * {
+ *   use(new Crash()
+ *      .plugin(HttpShellPlugin.class)
+ *   );
+ * }
+ * }
+ * + *

+ * Try it: + *

+ * + *
+ * GET /api/shell/thread/ls
+ * 
+ * + *

+ * OR: + *

+ * + *
+ * GET /api/shell/thread ls
+ * 
+ * + *

+ * The connector listen at /api/shell. If you want to mount the connector some + * where + * else just set the property: crash.httpshell.path. + *

+ * + *

SSH connector

+ *

+ * Just add the crash.connectors.ssh + * dependency to your project. + *

+ * + *

+ * Try it: + *

+ * + *
+ * ssh -p 2000 admin@localhost
+ * 
+ * + *

+ * Default user and password is: admin. See how to provide a custom + * authentication + * plugin. + *

+ * + *

telnet connector

+ *

+ * Just add the crash.connectors.telnet + * dependency to your project. + *

+ * + *

+ * Try it: + *

+ * + *
+ * telnet localhost 5000
+ * 
+ * + *

+ * Checkout complete + * telnet + * connector configuration. + *

+ * + *

web connector

+ *

+ * Just add the crash.connectors.web + * dependency to your project. + *

+ * + *

+ * Try it: + *

+ * + *
+ * GET /shell
+ * 
+ * + *

+ * A web shell console will be ready to go at /shell. If you want to mount the + * connector some where else just set the property: crash.webshell.path. + *

+ * + * @author edgar + * @since 1.0.0 + */ +@SuppressWarnings({"unchecked", "rawtypes" }) +public class Crash implements Module { + + static { + if (!SLF4JBridgeHandler.isInstalled()) { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + } + + private static final Key> PLUGINS = Key + .get(new TypeLiteral>() { + }); + + private final Set plugins = new HashSet<>(); + + private final ClassLoader loader; + + /** + * Creates a new {@link Crash} module. + * + * @param loader Class loader to use or null. + */ + public Crash(final ClassLoader loader) { + this.loader = Optional.ofNullable(loader).orElse(getClass().getClassLoader()); + } + + /** + * Creates a new {@link Crash} module. + */ + public Crash() { + this(null); + } + + /** + * Add a custom plugin to CRaSH. + * + * @param plugin Plugin class. + * @return This module. + */ + public Crash plugin(final Class plugin) { + plugins.add(plugin); + return this; + } + + @Override + public void configure(final Env env, final Config conf, final Binder binder) { + Properties props = new Properties(); + if (conf.hasPath("crash")) { + conf.getConfig("crash").entrySet().forEach( + e -> props.setProperty("crash." + e.getKey(), e.getValue().unwrapped().toString())); + } + + Map attributes = new HashMap<>(); + attributes.put("conf", conf); + + // connectors.web on classpath? + boolean webShell = loader.getResource("META-INF/resources/js/crash.js") != null; + if (webShell) { + plugins.add(WebShellPlugin.class); + WebShellPlugin.install(env, conf); + } + + if (plugins.contains(HttpShellPlugin.class)) { + HttpShellPlugin.install(env, conf); + } + + Multibinder mb = Multibinder.newSetBinder(binder, CRaSHPlugin.class); + plugins.forEach(it -> mb.addBinding().to(it)); + + CrashBootstrap crash = new CrashBootstrap(); + + Promise promise = Promise.make(); + binder.bind(PluginContext.class).toProvider(promise.future()::get); + + env.onStart(r -> { + Set routes = r.require(Route.KEY); + Set sockets = r.require(WebSocket.KEY); + attributes.put("registry", r); + attributes.put("routes", routes); + attributes.put("websockets", sockets); + attributes.put("env", env); + + Set plugins = Sets.newHashSet(r.require(PLUGINS)); + ServiceLoader.load(CRaSHPlugin.class, this.loader).forEach(plugins::add); + + promise.success(crash.start(loader, props, attributes, plugins)); + }); + + env.onStop(crash::stop); + } + + @Override + public Config config() { + return ConfigFactory.parseResources(getClass(), "crash.conf"); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java b/jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java new file mode 100644 index 0000000000..01ab724207 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import static java.util.Arrays.asList; +import static javaslang.Tuple.of; +import static org.jooby.crash.CrashFSDriver.endsWith; +import static org.jooby.crash.CrashFSDriver.noneOf; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +import org.crsh.plugin.CRaSHPlugin; +import org.crsh.plugin.PluginContext; +import org.crsh.plugin.PluginLifeCycle; +import org.crsh.vfs.FS; + +import javaslang.control.Try; + +class CrashBootstrap extends PluginLifeCycle { + + private static final Predicate ACCEPT = endsWith(".class").negate(); + + private List drivers = new ArrayList<>(); + + public PluginContext start(final ClassLoader loader, final Properties props, + final Map attributes, final Set> plugins) throws IOException { + FS conffs = newFS(CrashFSDriver.parse(loader, asList( + of("crash", ACCEPT)))); + + FS cmdfs = newFS(CrashFSDriver.parse(loader, asList( + of("cmd", ACCEPT), + of("org/jooby/crash", ACCEPT), + of("crash/commands", noneOf("jndi.groovy", "jdbc.groovy", "jpa.groovy", "jul.groovy"))))); + + setConfig(props); + + PluginContext ctx = new PluginContext(executor("crash"), scanner("crash-scanner"), + () -> plugins, attributes, cmdfs, conffs, loader); + + ctx.refresh(); + + start(ctx); + + return ctx; + } + + public void shutdown() { + drivers.forEach(it -> Try.run(it::close)); + super.stop(); + } + + private FS newFS(final List drivers) throws IOException { + FS fs = new FS(); + for (CrashFSDriver driver : drivers) { + fs.mount(driver); + } + this.drivers.addAll(drivers); + return fs; + } + + private static ScheduledExecutorService scanner(final String name) { + return Executors.newScheduledThreadPool(1, r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName(name); + return thread; + }); + } + + private static ExecutorService executor(final String name) { + AtomicInteger next = new AtomicInteger(0); + return new ThreadPoolExecutor(0, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue(), + r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName(name + "-" + next.incrementAndGet()); + return thread; + }); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java b/jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java new file mode 100644 index 0000000000..276bca351c --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java @@ -0,0 +1,207 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.crsh.vfs.spi.AbstractFSDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; + +import javaslang.Predicates; +import javaslang.Tuple2; +import javaslang.control.Try; + +class CrashFSDriver extends AbstractFSDriver implements AutoCloseable { + + private static final String LOGIN = "login.groovy"; + + private static Predicate FLOGIN = fname(LOGIN); + + private static final String JAR = ".jar!"; + + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + private Path root; + + private FileSystem fs; + + private Predicate filter; + + private URI src; + + private CrashFSDriver(final URI src, final FileSystem fs, final Predicate filter) { + this.src = src; + this.fs = fs; + String path = src.toString(); + int jar = path.indexOf(JAR); + path = path.substring(jar + JAR.length()); + this.root = fs.getPath(path); + this.filter = filter; + } + + private CrashFSDriver(final URI src, final Path root, final Predicate filter) { + this.src = src; + this.root = root; + this.filter = filter; + } + + @Override + public Path root() throws IOException { + return root; + } + + @Override + public String name(final Path handle) throws IOException { + return Optional.ofNullable(handle.getFileName()).orElse(handle).toString().replaceAll("/", ""); + } + + @Override + public boolean isDir(final Path handle) throws IOException { + return Files.isDirectory(handle); + } + + @Override + public Iterable children(final Path handle) throws IOException { + try (Stream walk = Files.walk(handle)) { + List children = walk + .skip(1) + .filter(filter) + .collect(Collectors.toList()); + return children; + } + } + + public boolean exists(final Predicate filter) { + try (Stream walk = Try.of(() -> Files.walk(root)).getOrElse(Stream.empty())) { + return walk + .skip(1) + .filter(filter) + .findFirst() + .isPresent(); + } + } + + @Override + public long getLastModified(final Path handle) throws IOException { + return Try.of(() -> Files.getLastModifiedTime(handle).toMillis()).getOrElse(-1L); + } + + @Override + public Iterator open(final Path handle) throws IOException { + return ImmutableList.of(Files.newInputStream(handle)).iterator(); + } + + @Override + public void close() throws Exception { + if (fs != null) { + fs.close(); + } + } + + @Override + public String toString() { + return root + " -> " + src; + } + + public static List parse(final ClassLoader loader, + final List>> paths) { + List drivers = new ArrayList<>(); + boolean loginFound = false; + + for (Tuple2> path : paths) { + for (URI uri : expandPath(loader, path._1)) { + // classpath first + CrashFSDriver driver = Try.of(() -> FileSystems.newFileSystem(uri, Collections.emptyMap())) + .map(it -> Try.of(() -> new CrashFSDriver(uri, it, path._2)).get()) + // file system fallback + .recoverWith(x -> Try.of(() -> new CrashFSDriver(uri, Paths.get(uri), path._2))) + .get(); + // HACK to make sure login.groovy takes precedence over default one + driver.log.debug("driver created: {}", driver); + if (loginFound) { + driver.filter = FLOGIN.negate().and(driver.filter); + } else if (driver.exists(FLOGIN)) { + driver.log.debug(" login.groovy found: {}", driver); + loginFound = true; + } + drivers.add(driver); + } + } + return drivers; + } + + private static List expandPath(final ClassLoader loader, final String pattern) { + List result = new ArrayList<>(); + File file = new File(pattern); + if (file.exists()) { + result.add(file.toURI()); + } + Try.run(() -> { + Collections.list(loader.getResources(pattern)) + .stream() + .map(it -> Try.of(() -> it.toURI()).get()) + .forEach(result::add); + }); + return result; + } + + @SuppressWarnings({"rawtypes", "unchecked" }) + public static Predicate noneOf(final String... names) { + Predicate[] predicates = new Predicate[names.length]; + for (int i = 0; i < predicates.length; i++) { + predicates[i] = fname(names[i]); + } + return Predicates.noneOf(predicates); + } + + public static Predicate fname(final String name) { + return it -> Optional.ofNullable(it.getFileName()) + .map(Object::toString) + .orElse(it.toString()) + .equals(name); + } + + public static Predicate endsWith(final String ext) { + return it -> Optional.ofNullable(it.getFileName()) + .map(Object::toString) + .orElse(it.toString()) + .endsWith(ext); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java b/jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java new file mode 100644 index 0000000000..cc7777f6b8 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import org.crsh.plugin.CRaSHPlugin; +import org.crsh.plugin.PluginContext; +import org.crsh.shell.Shell; +import org.crsh.shell.ShellFactory; +import org.crsh.shell.ShellProcess; +import org.crsh.shell.ShellProcessContext; +import org.jooby.Deferred; +import org.jooby.Env; +import org.jooby.MediaType; + +import com.typesafe.config.Config; + +public class HttpShellPlugin extends CRaSHPlugin { + + @Override + public HttpShellPlugin getImplementation() { + return this; + } + + static void install(final Env env, final Config conf) { + String path = conf.getString("crash.httpshell.path"); + env.routes().get(path + "/{cmd:.*}", req -> { + MediaType type = req.accepts(MediaType.json).map(it -> MediaType.json) + .orElse(MediaType.plain); + + return new Deferred(deferred -> { + PluginContext ctx = req.require(PluginContext.class); + ShellFactory shellFactory = ctx.getPlugin(ShellFactory.class); + Shell shell = shellFactory.create(null); + String cmd = req.param("cmd").value().replaceAll("/", " "); + ShellProcess process = shell.createProcess(cmd); + ShellProcessContext spc = new SimpleProcessContext( + result -> deferred.resolve(result.type(type))); + process.execute(spc); + }); + }); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java b/jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java new file mode 100644 index 0000000000..9d25fc4cca --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java @@ -0,0 +1,135 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import static javaslang.API.$; +import static javaslang.API.Case; +import static javaslang.API.Match; +import static javaslang.Predicates.instanceOf; + +import java.io.IOException; +import java.util.function.Consumer; + +import org.crsh.shell.ShellProcessContext; +import org.crsh.shell.ShellResponse; +import org.crsh.text.Screenable; +import org.crsh.text.Style; +import org.jooby.Result; +import org.jooby.Results; + +class SimpleProcessContext implements ShellProcessContext { + + private StringBuilder buff = new StringBuilder(); + + private Consumer deferred; + + private int width; + + private int height; + + public SimpleProcessContext(final Consumer deferred) { + this(deferred, 204, 48); + } + + public SimpleProcessContext(final Consumer deferred, final int width, + final int height) { + this.deferred = deferred; + this.width = width; + this.height = height; + } + + @Override + public boolean takeAlternateBuffer() throws IOException { + return false; + } + + @Override + public boolean releaseAlternateBuffer() throws IOException { + return false; + } + + @Override + public String getProperty(final String propertyName) { + return null; + } + + @Override + public String readLine(final String msg, final boolean echo) + throws IOException, InterruptedException, IllegalStateException { + return null; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void flush() throws IOException { + } + + @Override + public Screenable append(final Style style) throws IOException { + return this; + } + + @Override + public Screenable cls() throws IOException { + return this; + } + + @Override + public Appendable append(final CharSequence csq) throws IOException { + buff.append(csq); + return this; + } + + @Override + public Appendable append(final CharSequence csq, final int start, final int end) + throws IOException { + buff.append(csq, start, end); + return this; + } + + @Override + public Appendable append(final char c) throws IOException { + buff.append(c); + return this; + } + + @Override + public void end(final ShellResponse response) { + org.jooby.Status status = Match(response).of( + Case(instanceOf(ShellResponse.Ok.class), org.jooby.Status.OK), + Case(instanceOf(ShellResponse.UnknownCommand.class), org.jooby.Status.BAD_REQUEST), + Case($(), org.jooby.Status.SERVER_ERROR)); + + deferred.accept(Results.with(buff.length() == 0 ? response.getMessage() : buff, status)); + } + + @Override + public String toString() { + return buff.toString(); + } +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java b/jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java new file mode 100644 index 0000000000..d06bf4c968 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java @@ -0,0 +1,142 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.crsh.cli.impl.Delimiter; +import org.crsh.cli.impl.completion.CompletionMatch; +import org.crsh.cli.spi.Completion; +import org.crsh.plugin.PluginContext; +import org.crsh.shell.Shell; +import org.crsh.shell.ShellFactory; +import org.crsh.shell.ShellProcess; +import org.crsh.util.Utils; +import org.jooby.Request; +import org.jooby.WebSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableMap; + +import javaslang.control.Try; + +class WebShellHandler implements WebSocket.FullHandler { + + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + @SuppressWarnings("rawtypes") + @Override + public void connect(final Request req, final WebSocket ws) throws Exception { + PluginContext ctx = req.require(PluginContext.class); + ShellFactory factory = ctx.getPlugin(ShellFactory.class); + Shell shell = factory.create(null); + + AtomicReference process = new AtomicReference(); + + ws.onMessage(msg -> { + Map event = msg.to(Map.class); + String type = (String) event.get("type"); + if (type.equals("welcome")) { + log.debug("sending welcome + prompt"); + ws.send(event("print", shell.getWelcome())); + ws.send(event("prompt", shell.getPrompt())); + } else if (type.equals("execute")) { + String command = (String) event.get("command"); + Integer width = (Integer) event.get("width"); + Integer height = (Integer) event.get("height"); + process.set(shell.createProcess(command)); + SimpleProcessContext context = new SimpleProcessContext(r -> { + Try.run(() -> { + // reset process + process.set(null); + + ws.send(event("print", r.get())); + ws.send(event("prompt", shell.getPrompt())); + ws.send(event("end")); + }).onFailure(x -> log.error("error found while sending output", x)); + + if ("bye".equals(command)) { + ws.close(WebSocket.NORMAL); + } + }, width, height); + log.debug("executing {}", command); + process.get().execute(context); + } else if (type.equals("cancel")) { + ShellProcess p = process.get(); + if (p != null) { + log.info("cancelling {}", p); + p.cancel(); + } + } else if (type.equals("complete")) { + String prefix = (String) event.get("prefix"); + CompletionMatch completion = shell.complete(prefix); + Completion completions = completion.getValue(); + Delimiter delimiter = completion.getDelimiter(); + StringBuilder sb = new StringBuilder(); + List values = new ArrayList(); + if (completions.getSize() == 1) { + String value = completions.getValues().iterator().next(); + delimiter.escape(value, sb); + if (completions.get(value)) { + sb.append(delimiter.getValue()); + } + values.add(sb.toString()); + } else { + String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues()); + if (commonCompletion.length() > 0) { + delimiter.escape(commonCompletion, sb); + values.add(sb.toString()); + } else { + for (Map.Entry entry : completions) { + delimiter.escape(entry.getKey(), sb); + values.add(sb.toString()); + sb.setLength(0); + } + } + } + log.debug("completing {} with {}", prefix, values); + ws.send(event("complete", values)); + } + }); + + // clean up on close + ws.onClose(status -> { + log.info("closing web-socket"); + ShellProcess sp = process.get(); + if (sp != null) { + sp.cancel(); + } + shell.close(); + }); + } + + private Object event(final String type, final Object data) { + return ImmutableMap.of("type", type, "data", data); + } + + private Object event(final String type) { + return ImmutableMap.of("type", type); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java b/jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java new file mode 100644 index 0000000000..14f170ab20 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.jooby.crash; + +import org.crsh.plugin.CRaSHPlugin; +import org.jooby.Env; +import org.jooby.MediaType; +import org.jooby.Results; +import org.jooby.Route; +import org.jooby.Routes; + +import com.typesafe.config.Config; + +class WebShellPlugin extends CRaSHPlugin { + + @Override + public WebShellPlugin getImplementation() { + return this; + } + + static void install(final Env env, final Config conf) { + Routes routes = env.routes(); + String path = conf.getString("crash.webshell.path"); + String title = conf.getString("application.name") + " shell"; + routes.assets(path + "/css/**", "META-INF/resources/css/{0}"); + routes.assets(path + "/js/**", "META-INF/resources/js/{0}"); + String rootpath = Route.normalize(conf.getString("application.path") + path); + + routes.get(path, req -> Results.ok("\n" + + "\n" + + "\n" + + " \n" + + " " + title + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "").type(MediaType.html)); + + routes.ws(path, new WebShellHandler()).consumes(MediaType.json).produces(MediaType.json); + } + +} diff --git a/jooby-crash/src/main/resources/org/jooby/crash/conf.groovy b/jooby-crash/src/main/resources/org/jooby/crash/conf.groovy new file mode 100644 index 0000000000..0ce426e51d --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/conf.groovy @@ -0,0 +1,78 @@ +package org.jooby.crash + +import org.crsh.cli.Usage +import org.crsh.cli.Command +import org.crsh.cli.completers.SystemPropertyNameCompleter +import org.crsh.command.InvocationContext +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import org.crsh.cli.Man +import org.crsh.cli.Argument +import org.crsh.cli.Option + +import org.crsh.cli.completers.EnumCompleter +import java.util.regex.Pattern +import org.crsh.cli.Required + +import com.google.common.base.Strings; + +@Usage("application properties commands") +class conf +{ + + @Usage("print configuration tree") + @Command + Object tree() { + return configTree(context.attributes.conf.origin().description()) + } + + @Usage("print properties") + @Command + void props(InvocationContext context, + @Usage("Apply a path filter, either a property name or config sub-tree") + @Argument + String path) { + + def conf = context.attributes.conf + try { + def local = path == null ? conf : conf.getConfig(path) + def prefix = path == null ? "" : path + "." + + local.entrySet().each { + context.provide([name: prefix + it.key, value: it.value.unwrapped()]) + } + } catch (Exception x) { + context.provide([name: path, value: conf.getAnyRef(path)]) + } + } + + private String configTree(final String description) { + return configTree(description.split(":\\s+\\d+,|,"), 0); + } + + private String configTree(final String[] sources, final int i) { + if (i < sources.length) { + return new StringBuilder() + .append(Strings.padStart("", i, (char)' ')) + .append("└── ") + .append(sources[i]) + .append("\n") + .append(configTree(sources, i + 1)) + .toString(); + } + return ""; + } + +} + +@Retention(RetentionPolicy.RUNTIME) +@Usage("the property name") +@Man("The name of the property") +@Argument(name = "name", completer = SystemPropertyNameCompleter.class) +@interface PropertyName { } + +@Retention(RetentionPolicy.RUNTIME) +@Usage("the property value") +@Man("The value of the property") +@Argument(name = "value") +@interface PropertyValue { } diff --git a/jooby-crash/src/main/resources/org/jooby/crash/crash.conf b/jooby-crash/src/main/resources/org/jooby/crash/crash.conf new file mode 100644 index 0000000000..61ba6fbac5 --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/crash.conf @@ -0,0 +1,2 @@ +crash.webshell.path = /shell +crash.httpshell.path = /api/shell diff --git a/jooby-crash/src/main/resources/org/jooby/crash/login.groovy b/jooby-crash/src/main/resources/org/jooby/crash/login.groovy new file mode 100644 index 0000000000..49a07c3cbe --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/login.groovy @@ -0,0 +1,19 @@ +welcome = { -> + def registry = crash.context.attributes.registry + def conf = crash.context.attributes.conf + def appname = conf.getString("application.name") + def version = conf.getString("application.version") + + try { + return registry.require("application.banner", String.class) + " v" + version + "\n" + } catch (Exception x) { + return """ +Welcome to $appname v$version +""" + } +} + +prompt = { -> + def env = crash.context.attributes.conf.getString("application.env") + return env + "> "; +} \ No newline at end of file diff --git a/jooby-crash/src/main/resources/org/jooby/crash/routes.groovy b/jooby-crash/src/main/resources/org/jooby/crash/routes.groovy new file mode 100644 index 0000000000..aca1836ef2 --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/routes.groovy @@ -0,0 +1,43 @@ +package org.jooby.crash + +import org.crsh.cli.Usage +import org.crsh.cli.Command + +import static javaslang.API.$; +import static javaslang.API.Case; +import static javaslang.API.Match; +import static javaslang.Predicates.instanceOf; + +import org.jooby.Route; + +class routes +{ + @Usage("print application routes") + @Command + void main(InvocationContext context) { + def routes = context.attributes.routes + + routes.each { + def order = 0; + def filter = it.filter() + def pattern = it.pattern() + + if (filter instanceof Route.Before) { + pattern = "{before}" + pattern + } else if (filter instanceof Route.After) { + pattern = "{after}" + pattern + } else if (filter instanceof Route.Complete) { + pattern = "{complete}" + pattern + } + + context.provide([order:order, method: it.method(), pattern: pattern, consumes: it.consumes(), produces: it.produces(), name: it.name(), source: it.source()]) + order += 1 + } + + context.provide('\n\n') + def sockets = context.attributes.websockets + sockets.each { + context.provide([method: 'WS', pattern: it.pattern, consumes: it.consumes(), produces: it.produces()]) + } + } +} diff --git a/jooby-crash/src/test/java/app/AuthPlugin.java b/jooby-crash/src/test/java/app/AuthPlugin.java new file mode 100644 index 0000000000..7db7039007 --- /dev/null +++ b/jooby-crash/src/test/java/app/AuthPlugin.java @@ -0,0 +1,35 @@ +package app; + +import org.crsh.auth.AuthenticationPlugin; +import org.crsh.plugin.CRaSHPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AuthPlugin extends CRaSHPlugin + implements AuthenticationPlugin { + + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Override + public AuthPlugin getImplementation() { + return this; + } + + @Override + public String getName() { + return "auth"; + } + + @Override + public Class getCredentialType() { + return String.class; + } + + @Override + public boolean authenticate(final String username, final String credential) throws Exception { + log.info("{} pass {}", username, credential); + return true; + } + +} diff --git a/jooby-crash/src/test/java/app/CrashApp.java b/jooby-crash/src/test/java/app/CrashApp.java new file mode 100644 index 0000000000..73e16d18fa --- /dev/null +++ b/jooby-crash/src/test/java/app/CrashApp.java @@ -0,0 +1,38 @@ +package app; + +import org.jooby.Jooby; +import org.jooby.banner.Banner; +import org.jooby.crash.HttpShellPlugin; +import org.jooby.crash.Crash; +import org.jooby.json.Jackson; + +public class CrashApp extends Jooby { + { + conf("crash.conf"); + + use(new Jackson()); + + use(new Banner("crash me")); + + use(new Crash() + .plugin(HttpShellPlugin.class) + .plugin(AuthPlugin.class)); + + before("/path", (req, rsp) -> { + }); + + after("/path", (req, rsp, v) -> { + return v; + }); + + complete("/path", (req, rsp, v) -> { + }); + + get("/", () -> "OK"); + + } + + public static void main(final String[] args) { + run(CrashApp::new, args); + } +} diff --git a/jooby-crash/src/test/resources/crash.conf b/jooby-crash/src/test/resources/crash.conf new file mode 100644 index 0000000000..188edbcf8a --- /dev/null +++ b/jooby-crash/src/test/resources/crash.conf @@ -0,0 +1,8 @@ +crash.auth=auth +crash.mail.smtp.host=smtp.gmail.com +crash.mail.smtp.port=587 +crash.mail.smtp.secure=tls +crash.mail.smtp.username="espina.edgar@gmail.com" +crash.mail.smtp.password=th3w0rld1sm1n3 +crash.mail.smtp.from="espina.edgar@gmail.com" +crash.mail.debug=true diff --git a/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml b/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml index f1938901c6..d5d3a7cb63 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml @@ -83,6 +83,16 @@ + + + ${project.basedir}${file.separator}cmd + + cmd + + **/* + + + \ No newline at end of file diff --git a/jooby-dist/src/main/resources/assemblies/jooby.stork.xml b/jooby-dist/src/main/resources/assemblies/jooby.stork.xml index ccd5631a49..7ca9e824ba 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.stork.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.stork.xml @@ -55,6 +55,17 @@ + + + ${project.basedir}${file.separator}cmd + + cmd + true + + **/* + + + ${project.basedir}${file.separator}conf @@ -92,16 +103,6 @@ lib runtime - - *:* - - - - lib - compile - - *:* - diff --git a/jooby-dist/src/main/resources/assemblies/jooby.war.xml b/jooby-dist/src/main/resources/assemblies/jooby.war.xml index 6a7c0a5a03..feb6229c5e 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.war.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.war.xml @@ -141,6 +141,17 @@ + + + + ${project.build.outputDirectory} + + WEB-INF${file.separator}cmd + + **/* + + + diff --git a/jooby-dist/src/main/resources/assemblies/jooby.zip.xml b/jooby-dist/src/main/resources/assemblies/jooby.zip.xml index 8bc810d3f3..b4be06585e 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.zip.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.zip.xml @@ -58,6 +58,16 @@ + + + ${project.basedir}${file.separator}cmd + + cmd + + **/* + + + ${project.basedir}${file.separator}public diff --git a/md/doc/crash/crash.md b/md/doc/crash/crash.md new file mode 100644 index 0000000000..9d239218c0 --- /dev/null +++ b/md/doc/crash/crash.md @@ -0,0 +1,135 @@ +# crash + +CRaSH remote shell: connect and monitor JVM resources via HTTP, SSH or telnet. + +## dependency + +```xml + + org.jooby + jooby-crash + {{version}} + +``` + +## usage + +```java + +import org.jooby.crash; + +{ + use(new Crash()); +} +``` + +Just drop your commands in the `cmd` folder and CRaSH will pick up all them. + +Now let's see how to connect and interact with the CRaSH shell. + +## connectors + +### HTTP connector + +The HTTP connector is a simple yet powerful collection of HTTP endpoints where you can run a CRaSH command: + +```java +{ + use(new Crash() + .plugin(HttpShellPlugin.class) + ); +} +``` + +Try it: + +``` +GET /api/shell/thread/ls +``` + +OR: + +``` +GET /api/shell/thread ls +``` + +The connector is listening at ```/api/shell```. If you want to mount the connector some where else just set the property: ```crash.httpshell.path```. + +### SSH connector + +Just add the crash.connectors.ssh dependency to your project. + +Try it: + +``` +ssh -p 2000 admin@localhost +``` + +Default user and password is: ```admin```. See how to provide a custom authentication plugin. + +### telnet connector + +Just add the crash.connectors.telnet dependency to your project. + +Try it: + +``` +telnet localhost 5000 +``` + +Checkout complete telnet connector configuration. + +### web connector + +Just add the crash.connectors.web dependency to your project. + +Try it: + +``` +GET /shell +``` + +A web shell console will be ready to go at ```/shell```. If you want to mount the connector some where else just set the property: ```crash.webshell.path```. + +## commands + +You can write additional shell commands using Groovy or Java, see the CRaSH documentation for details. CRaSH search for commands in the ```cmd``` folder. + +Here is a simple ‘hello’ command that could be loaded from ```cmd/hello.groovy``` folder: + +```java +package commands + +import org.crsh.cli.Command +import org.crsh.cli.Usage +import org.crsh.command.InvocationContext + +class hello { + + @Usage("Say Hello") + @Command + def main(InvocationContext context) { + return "Hello" + } + +} +``` + +Jooby adds some additional attributes and commands to `InvocationContext` that you can access from your command: + +* registry: Access to [Registry]({{defdocs}}/Registry.html). +* conf: Access to `Config`. + +### routes command + +The ```routes``` print all the application routes. + +### conf command + +The ```conf tree``` print the application configuration tree (configuration precedence). + +The ```conf props [path]``` print all the application properties, sub-tree or a single property if ```path``` argument is present. + +## fancy banner + +Just add the [jooby-banner](/doc/banner) to your project and all the `CRaSH` shell will use it. Simple and easy!! diff --git a/md/doc/deployment/deployment.md b/md/doc/deployment/deployment.md index a67b0d1401..929ea4eeae 100644 --- a/md/doc/deployment/deployment.md +++ b/md/doc/deployment/deployment.md @@ -94,6 +94,8 @@ It works for ```logback.xml``` too, if ```logback.[env].xml``` is present, then {{war.md}} +{{doc/crash/crash.md}} + # conclusion * **jar/capsule deployment** makes perfect sense for PaaS like Heroku, AppEngine, etc... diff --git a/md/doc/more/more.md b/md/doc/more/more.md index 98a46b0df6..51fd3ea2fb 100644 --- a/md/doc/more/more.md +++ b/md/doc/more/more.md @@ -1,5 +1,6 @@ -# metrics +# monitor +* [crash](/doc/crash): [CRaSH](http://www.crashub.org/) remote shell integration. * [metrics](/doc/metrics): application metrics from the excellent [metrics](http://metrics.dropwizard.io) library. # utils diff --git a/pom.xml b/pom.xml index 3da8b6a5b9..09f9a92a16 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,7 @@ jooby-scanner jooby-csl jooby-unbescape + jooby-crash coverage-report @@ -566,6 +567,12 @@ ${jooby.version} + + org.jooby + jooby-crash + ${jooby.version} + + com.coverity.security @@ -1388,6 +1395,82 @@ ${jfiglet.version} + + + org.codehaus.groovy + groovy + ${groovy.version} + + + + + org.crashub + crash.shell + ${crash.version} + + + org.codehaus.groovy + groovy-all + + + + + + org.crashub + crash.connectors.telnet + ${crash.version} + + + org.codehaus.groovy + groovy-all + + + log4j + log4j + + + commons-logging + commons-logging + + + + + + org.crashub + crash.connectors.web + ${crash.version} + + + com.google.code.gson + gson + + + + + + org.crashub + crash.connectors.ssh + ${crash.version} + + + org.codehaus.groovy + groovy-all + + + + + + org.crashub + crash.plugins.mail + ${crash.version} + + + + org.crashub + crash.plugins.cron + ${crash.version} + + junit @@ -2500,6 +2583,8 @@ org.eclipse.jdt.apt.processorOptions/defaultOverwrite=true 1.0.0 0.7 2.5.0.M3 + 1.3.1 + 2.4.7 ** From 7684b782c76710c4b39d468d2ee1894ec6751375 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 15 Oct 2016 13:18:23 -0300 Subject: [PATCH 2/4] better assets handler initial setup, using onStart callback --- .../org/jooby/EnvConfCallbackFeature.java | 26 ++++++++++++ jooby/src/main/java/org/jooby/Jooby.java | 3 +- jooby/src/test/java/org/jooby/JoobyTest.java | 41 ++++++++++++------- 3 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java diff --git a/coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java b/coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java new file mode 100644 index 0000000000..af5e89068c --- /dev/null +++ b/coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java @@ -0,0 +1,26 @@ +package org.jooby; + +import org.jooby.test.ServerFeature; +import org.junit.Test; + +import com.google.inject.Key; +import com.google.inject.name.Names; + +public class EnvConfCallbackFeature extends ServerFeature { + + { + Key key = Key.get(String.class, Names.named(("envcallback"))); + on("dev", conf -> { + use((env, config, binder) -> { + binder.bind(key).toInstance(env.name()); + }); + }); + + get("/", req -> req.require(key)); + } + + @Test + public void devCallback() throws Exception { + request().get("/").expect("dev"); + } +} diff --git a/jooby/src/main/java/org/jooby/Jooby.java b/jooby/src/main/java/org/jooby/Jooby.java index 4eaeb7d505..601fdf86ec 100644 --- a/jooby/src/main/java/org/jooby/Jooby.java +++ b/jooby/src/main/java/org/jooby/Jooby.java @@ -3129,7 +3129,8 @@ public Route.Definition assets(final String path) { @Override public Route.Definition assets(final String path, final String location) { AssetHandler handler = new AssetHandler(location); - on("*", conf -> { + onStart(r -> { + Config conf = r.require(Config.class); handler .cdn(conf.getString("assets.cdn")) .lastModified(conf.getBoolean("assets.lastModified")) diff --git a/jooby/src/test/java/org/jooby/JoobyTest.java b/jooby/src/test/java/org/jooby/JoobyTest.java index ce745125d8..fed9799dcf 100644 --- a/jooby/src/test/java/org/jooby/JoobyTest.java +++ b/jooby/src/test/java/org/jooby/JoobyTest.java @@ -524,6 +524,7 @@ public Object m1() { expect(injector.getInstance(Route.KEY)).andReturn(Collections.emptySet()); expect(injector.getInstance(WebSocket.KEY)).andReturn(Collections.emptySet()); injector.injectMembers(isA(Jooby.class)); + unit.registerMock(Injector.class, injector); AppPrinter printer = unit.constructor(AppPrinter.class) .args(Set.class, Set.class, Config.class) @@ -2062,7 +2063,8 @@ public void assets() throws Exception { .expect(requestScope) .expect(webSockets) .expect(tmpdir) - .expect(err).expect(executor("deferred")) + .expect(err) + .expect(executor("deferred")) .expect(unit -> { Mutant ifModifiedSince = unit.mock(Mutant.class); expect(ifModifiedSince.toOptional(Long.class)).andReturn(Optional.empty()); @@ -2084,6 +2086,15 @@ public void assets() throws Exception { Route.Chain chain = unit.get(Route.Chain.class); chain.next(req, rsp); }) + .expect(unit -> { + Config conf = unit.get(Config.class); + expect(conf.getString("assets.cdn")).andReturn("").times(2); + expect(conf.getBoolean("assets.lastModified")).andReturn(true).times(2); + expect(conf.getBoolean("assets.etag")).andReturn(true).times(2); + + Injector injector = unit.get(Injector.class); + expect(injector.getInstance(Key.get(Config.class))).andReturn(conf).times(2); + }) .run(unit -> { Jooby jooby = new Jooby(); @@ -2259,23 +2270,23 @@ public void ws() throws Exception { .expect(routeHandler) .expect(params) .expect(requestScope) - .expect( - unit -> { - Multibinder multibinder = unit.mock(Multibinder.class); + .expect(unit -> { + Multibinder multibinder = unit.mock(Multibinder.class); - LinkedBindingBuilder binding = unit - .mock(LinkedBindingBuilder.class); - binding.toInstance(unit.capture(WebSocket.Definition.class)); + LinkedBindingBuilder binding = unit + .mock(LinkedBindingBuilder.class); + binding.toInstance(unit.capture(WebSocket.Definition.class)); - expect(multibinder.addBinding()).andReturn(binding); + expect(multibinder.addBinding()).andReturn(binding); - Binder binder = unit.get(Binder.class); + Binder binder = unit.get(Binder.class); - expect(Multibinder.newSetBinder(binder, WebSocket.Definition.class)).andReturn( - multibinder); - }) + expect(Multibinder.newSetBinder(binder, WebSocket.Definition.class)).andReturn( + multibinder); + }) .expect(tmpdir) - .expect(err).expect(executor("deferred")) + .expect(err) + .expect(executor("deferred")) .run(unit -> { Jooby jooby = new Jooby(); @@ -2283,8 +2294,8 @@ public void ws() throws Exception { WebSocket.Definition ws = jooby.ws("/", (socket) -> { }); assertEquals("/", ws.pattern()); - assertEquals(MediaType.all, ws.consumes()); - assertEquals(MediaType.all, ws.produces()); + assertEquals(MediaType.plain, ws.consumes()); + assertEquals(MediaType.plain, ws.produces()); defs.add(ws); jooby.start(); From a92377c3e6d7503de9caa7ccb013944334d66f07 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sat, 15 Oct 2016 13:18:55 -0300 Subject: [PATCH 3/4] use websocket text/plain as default content-type fix #504 --- .../jooby/ws/OnByteArrayMessageFeature.java | 3 +- .../jooby/ws/OnByteBufferMessageFeature.java | 4 +-- .../java/org/jooby/banner/BannerTest.java | 22 ++++++++++++ jooby/src/main/java/org/jooby/Route.java | 16 +++++++-- jooby/src/main/java/org/jooby/WebSocket.java | 4 +-- .../org/jooby/internal/WebSocketImpl.java | 2 +- .../java/org/jooby/RouteDefinitionTest.java | 8 +++++ .../org/jooby/WebSocketDefinitionTest.java | 4 +-- .../org/jooby/internal/AppPrinterTest.java | 34 +++++++++---------- 9 files changed, 70 insertions(+), 27 deletions(-) diff --git a/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java b/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java index 223c5d2420..91930840c3 100644 --- a/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java +++ b/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java @@ -6,6 +6,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jooby.MediaType; import org.jooby.test.ServerFeature; import org.junit.After; import org.junit.Before; @@ -29,7 +30,7 @@ public class OnByteArrayMessageFeature extends ServerFeature { ws.close(); }); }); - }); + }).produces(MediaType.octetstream); } diff --git a/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java b/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java index 18d356b3fa..7cf87abf43 100644 --- a/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java +++ b/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java @@ -7,6 +7,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jooby.MediaType; import org.jooby.test.ServerFeature; import org.junit.After; import org.junit.Before; @@ -30,8 +31,7 @@ public class OnByteBufferMessageFeature extends ServerFeature { ws.close(); }); }); - - }); + }).produces(MediaType.octetstream); } diff --git a/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java b/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java index e49efd9a74..7b4e77dddc 100644 --- a/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java +++ b/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java @@ -1,6 +1,9 @@ package org.jooby.banner; import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; + +import javax.inject.Provider; import org.jooby.Env; import org.jooby.test.MockUnit; @@ -14,6 +17,9 @@ import com.github.lalyos.jfiglet.FigletFont; import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.name.Names; import com.typesafe.config.Config; import javaslang.control.Try.CheckedRunnable; @@ -34,6 +40,7 @@ public void configure() throws Exception { new MockUnit(Env.class, Config.class, Binder.class, Logger.class) .expect(conf("app", "1.0.0")) .expect(log("app")) + .expect(banner()) .expect(onStart) .run(unit -> { new Banner(banner) @@ -50,6 +57,7 @@ public void print() throws Exception { .expect(onStart) .expect(convertOnLine(banner, "speed")) .expect(print(banner, "1.0.0")) + .expect(banner()) .run(unit -> { new Banner(banner) .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class)); @@ -67,6 +75,7 @@ public void font() throws Exception { .expect(onStart) .expect(convertOnLine(banner, "myfont")) .expect(print(banner, "1.0.0")) + .expect(banner()) .run(unit -> { new Banner(banner) .font("myfont") @@ -85,6 +94,7 @@ public void defprint() throws Exception { .expect(onStart) .expect(convertOnLine(banner, "speed")) .expect(print(banner, "1.0.0")) + .expect(banner()) .run(unit -> { new Banner() .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class)); @@ -121,4 +131,16 @@ private Block conf(final String name, final String v) { expect(conf.getString("application.version")).andReturn(v); }; } + + @SuppressWarnings("unchecked") + private Block banner() { + return unit -> { + + LinkedBindingBuilder lbb = unit.mock(LinkedBindingBuilder.class); + expect(lbb.toProvider(isA(Provider.class))).andReturn(lbb); + + Binder binder = unit.get(Binder.class); + expect(binder.bind(Key.get(String.class, Names.named("application.banner")))).andReturn(lbb); + }; + } } diff --git a/jooby/src/main/java/org/jooby/Route.java b/jooby/src/main/java/org/jooby/Route.java index ba9f51b385..db8092aa4d 100644 --- a/jooby/src/main/java/org/jooby/Route.java +++ b/jooby/src/main/java/org/jooby/Route.java @@ -1008,7 +1008,8 @@ public Collection map(final Mapper mapper) { class Definition implements Props { private static final SourceProvider SRC = SourceProvider.DEFAULT_INSTANCE - .plusSkippedClasses(Definition.class, Jooby.class, Collection.class, Group.class); + .plusSkippedClasses(Definition.class, Jooby.class, Collection.class, Group.class, + javaslang.collection.List.class, Routes.class); /** * Route's name. @@ -1173,6 +1174,15 @@ public boolean glob() { return cpattern.glob(); } + /** + * Source information (where the route was defined). + * + * @return Source information (where the route was defined). + */ + public Route.Source source() { + return new RouteSourceImpl(declaringClass, line); + } + /** * Recreate a route path and apply the given variables. * @@ -2229,7 +2239,9 @@ static String normalize(final String path) { } /** - * @return Source information. + * Source information (where the route was defined). + * + * @return Source information (where the route was defined). */ Route.Source source(); diff --git a/jooby/src/main/java/org/jooby/WebSocket.java b/jooby/src/main/java/org/jooby/WebSocket.java index 628972bb35..e2b7711da2 100644 --- a/jooby/src/main/java/org/jooby/WebSocket.java +++ b/jooby/src/main/java/org/jooby/WebSocket.java @@ -279,13 +279,13 @@ class Definition { * Defines the media types that the methods of a resource class or can consumes. Default is: * {@literal *}/{@literal *}. */ - private MediaType consumes = MediaType.all; + private MediaType consumes = MediaType.plain; /** * Defines the media types that the methods of a resource class or can produces. Default is: * {@literal *}/{@literal *}. */ - private MediaType produces = MediaType.all; + private MediaType produces = MediaType.plain; /** A path pattern. */ private String pattern; diff --git a/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java b/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java index 3b90209314..e3a5474df4 100644 --- a/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java +++ b/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java @@ -151,7 +151,7 @@ public void connect(final Injector injector, final Request req, final NativeWebS ws.onTextMessage(message -> Try .run(() -> messageCallback.invoke( - new MutantImpl(injector.getInstance(ParserExecutor.class), + new MutantImpl(injector.getInstance(ParserExecutor.class), consumes, new StrParamReferenceImpl("body", "message", ImmutableList.of(message))))) .onFailure(this::handleErr)); diff --git a/jooby/src/test/java/org/jooby/RouteDefinitionTest.java b/jooby/src/test/java/org/jooby/RouteDefinitionTest.java index 7a9a08c904..72dc0eaf9e 100644 --- a/jooby/src/test/java/org/jooby/RouteDefinitionTest.java +++ b/jooby/src/test/java/org/jooby/RouteDefinitionTest.java @@ -316,6 +316,14 @@ public void attrs() throws Exception { assertEquals(Route.class, r.attr("type")); } + @Test + public void src() throws Exception { + Function route = path -> new Route.Definition("*", path, () -> null); + Route.Definition r = route.apply("/"); + + assertEquals("org.jooby.RouteDefinitionTest:321", r.source().toString()); + } + @Test public void glob() throws Exception { Function route = path -> new Route.Definition("*", path, () -> null); diff --git a/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java b/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java index dbf58c22a3..d04f2d7c5f 100644 --- a/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java +++ b/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java @@ -13,8 +13,8 @@ public void toStr() { }); assertEquals("WS /pattern\n" + - " consume: */*\n" + - " produces: */*\n", def.toString()); + " consume: text/plain\n" + + " produces: text/plain\n", def.toString()); } @Test diff --git a/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java b/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java index 3eb40f26c1..8308535d15 100644 --- a/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java +++ b/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java @@ -32,7 +32,7 @@ public void print() { " GET {complete}/ [*/*] [*/*] (/anonymous)\n" + " GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + "listening on:\n" + " http://localhost:8080/", setup); @@ -58,11 +58,11 @@ public void printHttps() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/" + - "\n https://localhost:8443/", setup); + "listening on:\n" + + " http://localhost:8080/\n" + + " https://localhost:8443/", setup); } @Test @@ -76,11 +76,11 @@ public void printHttp2() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/ +h2" + - "\n https://localhost:8443/ +h2", setup); + "listening on:\n" + + " http://localhost:8080/ +h2\n" + + " https://localhost:8443/ +h2", setup); } @Test @@ -95,11 +95,11 @@ public void printHttp2Https() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/" + - "\n https://localhost:8443/ +h2", setup); + "listening on:\n" + + " http://localhost:8080/\n" + + " https://localhost:8443/ +h2", setup); } @Test @@ -113,10 +113,10 @@ public void printHttp2ClearText() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/ +h2", setup); + "listening on:\n" + + " http://localhost:8080/ +h2", setup); } private Config config(final String path) { @@ -136,7 +136,7 @@ public void printWithPath() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + "listening on:\n" + " http://localhost:8080/app", setup); From 421385afbef1a3d9cd9883bf985707628afd7847 Mon Sep 17 00:00:00 2001 From: leleuj Date: Fri, 21 Oct 2016 11:01:01 +0200 Subject: [PATCH 4/4] Security issue: upgrade to pac4j v1.9.4 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 216f2bca7b..916842f839 100644 --- a/pom.xml +++ b/pom.xml @@ -2553,7 +2553,7 @@ org.eclipse.jdt.apt.processorOptions/defaultOverwrite=true 2.2.6 2.6.2 2.10.0 - 1.9.2 + 1.9.4 1.5.8 2.1.8-M1 3.4