29 Commits

Author SHA1 Message Date
Justin Crawford
3e45f7ebf4 Tweak the SSH MOTD a bit 2019-10-03 22:16:29 -07:00
Justin Crawford
0e05bb61bc Support CTRL+D for exiting the console
Support CTRL+D for exiting the console, also support "cls" for
clearing the screen (on supported terminals).
2019-10-03 21:41:02 -07:00
Justin Crawford
0635ea7a35 Fixed a bug (open on the upstream fork), also rewote config.
Fixed a bug that caused sessions to get overwritten and some of them
would seem to freeze, the whole thing relied on undefined behavior.
This bug was a static variable that copied sessions all around globally.

Rewrote the config to support a few more options (the PasswordType is coming soon)
and explained how the new authorized_users files work.

Public key authentication now has the same number of retires that
password authentication has (this aligns with how OpenSSH does it)
and the number of retries can now be configured in the configuration.
2019-10-03 21:07:00 -07:00
Justin Crawford
25287b1580 Fix Travis 2019-10-02 19:41:31 -07:00
Justin Crawford
0458179597 Add support for authorized_keys files.
Each user can have a set of authorized keys for public key authentication.
This is better to support as it lets us use different algorithms and not
just RSA. In the age of security, it's good to have variety.

I also added additional libraries to support ed25519-based public keys.

I updated the SSH libraries so any upstream bug fixes are applied, fixed
some warnings and a few other things.
2019-10-02 19:14:56 -07:00
Haarolean
dc76da9ac1 Version bump 2018-05-06 19:50:19 +03:00
Haarolean
7330b1eead Fix codestyle 2018-05-06 19:42:57 +03:00
Haarolean
8f6319d979 Fixed #10
Fixed #6 once more
Added exit command alias
2018-05-06 19:42:39 +03:00
Haarolean
272e7cf8dc Improved logging 2018-04-13 23:13:02 +03:00
Tony
69d31175d4 Add SFTP support (#7)
Add SFTP support
2018-03-25 22:44:41 +03:00
Haarolean
16f5c063ce Replaced craftbukkit with bukkit api for travis 2018-03-25 22:11:01 +03:00
Haarolean
2f1e03d7a2 Enabled travis-ci 2018-03-25 21:31:59 +03:00
Haarolean
32f65a506e https://github.com/rmichela/Bukkit-SSHD/issues/6 fix 2018-03-25 19:47:35 +03:00
Haarolean
703c69b055 NPE fix 2018-03-01 20:31:57 +03:00
Roman Zabaluev
15faff3dd4 Merge pull request #5 from rmichela/rpc
Added RPC mode without preamble and annoying stuff.
2018-02-25 17:22:22 +03:00
Haarolean
dc8e49f643 No need of NMS imports. 2018-02-12 04:00:42 +03:00
Haarolean
2380c42089 Added RPC mode without preamble and annoying stuff. 2018-02-10 19:13:21 +03:00
Haarolean
02b700b07f Made maven update plugin version in plugin.yml 2017-11-15 21:20:47 +03:00
Haarolean
87cb4d8929 preamble fix for unix. JLine didn't send line reset character 2017-11-15 20:49:46 +03:00
Haarolean
ac6a552e95 Added a workaround for paper' colors. 2017-11-14 18:44:27 +03:00
Haarolean
0951c6f971 Fix for paperspigot. Added bundled jline. Works with both spigot and paperspigot now. 2017-11-14 17:06:20 +03:00
Haarolean
5a9a5245b9 Bumped version number. Works with 1.12. 2017-11-14 00:02:07 +03:00
toxuin
396351771c Builds and runs. 2017-11-13 18:03:46 +03:00
Haarolean
f034cef465 Attempted fix for 1.12. 2017-11-13 18:00:44 +03:00
Ryan Michela
956f0cc3ef Changed * to all 2014-09-04 23:01:47 -07:00
Ryan Michela
027605e743 Renamed "host" to "listenAddress" 2014-08-17 22:41:53 -07:00
Ryan Michela
be9c53bf6f Added specific interface binding 2014-08-03 13:20:00 -07:00
Ryan Michela
f9a3ddec43 Fix plugin for 1.7.x 2014-07-03 00:18:34 -07:00
Ryan Michela
2326c7e2cb get bukkit 1.7.2 to compile 2013-12-28 22:47:19 -08:00
23 changed files with 1063 additions and 370 deletions

68
.clangformat Normal file
View File

@@ -0,0 +1,68 @@
---
#BasedOnStyle: WebKit
TabWidth: '4'
IndentWidth: '4'
UseTab: 'Always'
AlignOperands: 'true'
AlignAfterOpenBracket: 'Align'
AlignConsecutiveAssignments: 'true'
AlignConsecutiveDeclarations: 'true'
AlignEscapedNewlines: 'Left'
AlignTrailingComments: 'true'
AllowAllParametersOfDeclarationOnNextLine: 'true'
AllowShortBlocksOnASingleLine: 'false'
AllowShortCaseLabelsOnASingleLine: 'false'
AllowShortFunctionsOnASingleLine: 'All'
AllowShortIfStatementsOnASingleLine: 'false'
AllowShortLoopsOnASingleLine: 'false'
AlwaysBreakAfterReturnType: 'None'
AlwaysBreakTemplateDeclarations: 'true'
AlwaysBreakBeforeMultilineStrings: 'false'
BinPackArguments: 'false'
BinPackParameters: 'false'
BreakBeforeBraces: 'Custom'
BraceWrapping:
AfterEnum: 'true'
AfterClass: 'true'
AfterControlStatement: 'true'
AfterStruct: 'true'
AfterFunction: 'true'
AfterNamespace: 'true'
AfterUnion: 'true'
AfterExternBlock: 'true'
BeforeCatch: 'true'
BeforeElse: 'true'
SplitEmptyRecord: 'false'
SplitEmptyNamespace: 'false'
SplitEmptyFunction: 'false'
BreakBeforeBinaryOperators: 'true'
BreakBeforeTernaryOperators: 'false'
BreakConstructorInitializersBeforeComma: 'false'
BreakBeforeInheritanceComma: 'false'
BreakStringLiterals: 'true'
ColumnLimit: '140'
CompactNamespaces: 'false'
Cpp11BracedListStyle: 'true'
ConstructorInitializerAllOnOneLineOrOnePerLine: 'false'
DerivePointerAlignment: 'false'
IndentCaseLabels: 'true'
IndentPPDirectives: 'AfterHash'
KeepEmptyLinesAtTheStartOfBlocks: 'true'
Language: 'Java'
NamespaceIndentation: 'All'
PointerAlignment: 'Right'
ReflowComments: 'true'
SortIncludes: 'true'
SortUsingDeclarations: 'true'
SpaceAfterCStyleCast: 'false'
SpaceAfterTemplateKeyword: 'false'
SpaceBeforeAssignmentOperators: 'true'
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: 'false'
SpacesInAngles: 'false'
SpacesInCStyleCastParentheses: 'false'
SpacesInContainerLiterals: 'false'
SpacesInParentheses: 'false'
SpacesInSquareBrackets: 'false'
Standard: 'Auto'
...

5
.travis.yml Normal file
View File

@@ -0,0 +1,5 @@
sudo: false
dist: trusty
language: java
jdk:
- oraclejdk8

View File

@@ -1,4 +1,70 @@
Bukkit-SSHD Spigot-SSHD
=========== ===========
An SSHD daemon embedded in a Bukkit plugin. [![Build Status](https://travis-ci.org/Justasic/Spigot-SSHD.svg?branch=master)](https://travis-ci.org/Justasic/Spigot-SSHD)
Have you ever wished you could remotely access your server's admin console without having to setup a complex remote access system? Now you can with SSHD.
SSHD securely exposes your Spigot admin console using the SSH protocol - the same protocol that serves as the secure foundation for nearly all remote server administration.
- Compatible with all ssh clients, regardless of operating system.
- Remotely view your server log in real-time.
- Remotely issue commands from the server console, just as if you were on the server itself.
- Supports multiple concurrent remote connections.
- Strong identity support using public key authentication.
- Remotely script your server by issuing one-off console commands with ssh.
## Why should I use SSHD?
- Your server runs on Windows.
- You are in a shared hosting environment that only gives you access to the - log files.
- You want to share access to your server console, but don't want to give anybody access to the machine its running on.
- You always wanted to use RCON, but want to see the server log as well.
- You are tired of running your server in a Screen session.
- You just want to access your server console using SSH.
## Configuration
- **listenAddress** - The network interface(s) SSHD should listen on. (Default all)
- **port** - Specify the port SSHD should listen on. (Default 22)
- **username/password** - The credentials used to log into the server console. (Default blank)
Note: By default, only public key authentication is enabled. This is the most secure authentication mode! Setting a username and password will make your server less secure.
## Setting Up Public Key Authentication
Setting up public key authentication with SSH requires first generating a public and private key pair and then installing just the public key on your Spigot server.
On Windows
1. TODO
On Linux/OS X
1. TODO
## Commands
None - just install and go.
## Permissions
None - SSHD uses cryptographic certificates or a secure username and password to verify remote access.
## Source Code
[Get the source on GitHub](https://github.com/Justasic/Spigot-SSHD "Source Code")
## Metrics
This plugin utilizes Hidendra's plugin metrics system. the following information is collected and sent to mcstats.org unless opted out:
- A unique identifier
- The server's version of Java
- Whether the server is in offline or online mode
- Plugin's version
- Server's version
- OS version/name and architecture
- core count for the CPU
- Number of players online
- Metrics version
Opting out of this service can be done by editing plugins/Plugin Metrics/config.yml and changing opt-out to true.

144
pom.xml
View File

@@ -5,15 +5,20 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.ryanmichela</groupId> <groupId>com.ryanmichela</groupId>
<artifactId>SSHD</artifactId> <artifactId>sshd</artifactId>
<version>1.0</version> <version>1.3.5</version>
<url>http://dev.bukkit.org/server-mods/sshd/</url> <url>https://github.com/Justasic/Bukkit-SSHD/</url>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- Repositories --> <!-- Repositories -->
<repositories> <repositories>
<repository> <repository>
<id>bukkit-repo</id> <id>spigot-repo</id>
<url>http://repo.bukkit.org/content/groups/public/</url> <url>https://hub.spigotmc.org/nexus/content/groups/public</url>
</repository> </repository>
</repositories> </repositories>
@@ -29,62 +34,135 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.bukkit</groupId> <groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId> <artifactId>bukkit</artifactId>
<version>1.6.4-R1.0</version> <version>1.14.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
<type>jar</type>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId> <artifactId>sshd-core</artifactId>
<version>0.9.0</version> <version>2.3.0</version>
<scope>compile</scope> <scope>compile</scope>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<!--<dependency>-->
<!--<groupId>org.bouncycastle</groupId>--> <dependency>
<!--<artifactId>bcprov-jdk16</artifactId>--> <groupId>org.apache.sshd</groupId>
<!--<version>1.46</version>--> <artifactId>sshd-mina</artifactId>
<!--</dependency>--> <version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-common</artifactId>
<version>2.3.0</version>
<scope>compile</scope>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>net.i2p.crypto</groupId>
<artifactId>eddsa</artifactId>
<version>0.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.28</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.28</version>
</dependency>
<dependency>
<groupId>jline</groupId>
<artifactId>jline</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<!-- Build --> <!-- Build -->
<build> <build>
<defaultGoal>clean package</defaultGoal>
<resources>
<resource>
<targetPath>.</targetPath>
<filtering>true</filtering>
<directory>${basedir}/src/main/resources</directory>
<includes>
<include>plugin.yml</include>
<include>config.yml</include>
</includes>
</resource>
</resources>
<plugins> <plugins>
<!-- Shade plugin -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId>
<artifactId>maven-shade-plugin</artifactId> <version>3.1.1</version>
<version>1.5</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
<goals> <goals>
<goal>shade</goal> <goal>single</goal>
</goals> </goals>
</execution> </execution>
</executions> </executions>
<configuration> <configuration>
<filters> <finalName>${project.name}-${project.version}</finalName>
<filter> <appendAssemblyId>false</appendAssemblyId>
<artifact>*:*</artifact> <descriptorRefs>
<excludes> <descriptorRef>jar-with-dependencies</descriptorRef>
<exclude>META-INF/*.SF</exclude> </descriptorRefs>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration> </configuration>
</plugin> </plugin>
<!-- Compile plugin --> <!-- Compile plugin -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version> <version>3.7.0</version>
<configuration> <configuration>
<source>1.7</source> <source>1.8</source>
<target>1.7</target> <target>1.8</target>
<showDeprecation>true</showDeprecation> <showDeprecation>true</showDeprecation>
</configuration> </configuration>
</plugin> </plugin>

View File

@@ -1,6 +1,6 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.session.ServerSession;
import java.util.HashMap; import java.util.HashMap;
@@ -10,30 +10,39 @@ import java.util.Map;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class ConfigPasswordAuthenticator implements PasswordAuthenticator { public class ConfigPasswordAuthenticator implements PasswordAuthenticator {
private Map<String, Integer> failCounts = new HashMap<String, Integer>();
@Override private Map<String, Integer> FailCounts = new HashMap<String, Integer>();
public boolean authenticate(String username, String password, ServerSession serverSession) {
if (SshdPlugin.instance.getConfig().getString("credentials." + username).equals(password)) {
failCounts.put(username, 0);
return true;
}
SshdPlugin.instance.getLogger().info("Failed login for " + username + " using password authentication.");
try { @Override
Thread.sleep(3000); public boolean authenticate(String username, String password, ServerSession ss)
if (failCounts.containsKey(username)) { {
failCounts.put(username, failCounts.get(username) + 1); if (SshdPlugin.instance.getConfig().getString("Credentials." + username).equals(password))
} else { {
failCounts.put(username, 1); FailCounts.put(username, 0);
} return true;
if (failCounts.get(username) >= 3) { }
failCounts.put(username, 0); SshdPlugin.instance.getLogger().info("Failed login for " + username + " using password authentication.");
serverSession.close(true);
} Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries");
} catch (InterruptedException e) {
// do nothing try
} {
return false; Thread.sleep(3000);
} if (this.FailCounts.containsKey(username))
this.FailCounts.put(username, this.FailCounts.get(username) + 1);
else
this.FailCounts.put(username, 1);
if (this.FailCounts.get(username) >= tries)
{
this.FailCounts.put(username, 0);
ss.close(true);
}
}
catch (InterruptedException e)
{
// do nothing
}
return false;
}
} }

View File

@@ -4,15 +4,16 @@ package com.ryanmichela.sshd;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
import jline.console.completer.Completer;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandMap; import org.bukkit.command.CommandMap;
import org.bukkit.craftbukkit.libs.jline.console.completer.Completer;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.logging.Level; import java.util.logging.Level;
public class ConsoleCommandCompleter implements Completer { public class ConsoleCommandCompleter implements Completer {
public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) {
Waitable<List<String>> waitable = new Waitable<List<String>>() { Waitable<List<String>> waitable = new Waitable<List<String>>() {
@Override @Override

View File

@@ -1,7 +1,8 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.sshd.server.Command; import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.command.CommandFactory;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.Environment; import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.ExitCallback;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -14,12 +15,14 @@ import java.io.OutputStream;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class ConsoleCommandFactory implements CommandFactory { public class ConsoleCommandFactory implements CommandFactory {
@Override @Override
public Command createCommand(String command) { public Command createCommand(ChannelSession cs, String command) {
return new ConsoleCommand(command); return new ConsoleCommand(command);
} }
public class ConsoleCommand implements Command { public class ConsoleCommand implements Command {
private String command; private String command;
private InputStream in; private InputStream in;
@@ -48,20 +51,19 @@ public class ConsoleCommandFactory implements CommandFactory {
} }
@Override @Override
public void start(Environment environment) throws IOException { public void start(ChannelSession cs, Environment environment) throws IOException {
try { try {
SshdPlugin.instance.getLogger().info("[U: " + environment.getEnv().get(Environment.ENV_USER) + "] " + command); SshdPlugin.instance.getLogger()
.info("[U: " + environment.getEnv().get(Environment.ENV_USER) + "] " + command);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
} catch (Exception e) { } catch (Exception e) {
SshdPlugin.instance.getLogger().severe("Error processing command from SSH"); SshdPlugin.instance.getLogger().severe("Error processing command from SSH -" + e.getMessage());
} finally { } finally {
callback.onExit(0); callback.onExit(0);
} }
} }
@Override @Override
public void destroy() { public void destroy(ChannelSession cn) {}
}
}
}
} }

View File

@@ -0,0 +1,103 @@
package com.ryanmichela.sshd;
/**
* Copyright 2013 Ryan Michela
*/
import org.bukkit.ChatColor;
import org.fusesource.jansi.Ansi;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.EnumMap;
import java.util.Map;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
public class ConsoleLogFormatter extends Formatter {
private SimpleDateFormat dateFormat;
private static final Map<ChatColor, String> replacements = new EnumMap<ChatColor, String>(ChatColor.class);
public ConsoleLogFormatter() {
this.dateFormat = new SimpleDateFormat("HH:mm:ss");
}
public static String ColorizeString(String str)
{
// ORIGINAL CODE FROM org.bukkit.craftbukkit.command.ColouredConsoleSender
replacements.put(ChatColor.BLACK, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLACK).boldOff().toString());
replacements.put(ChatColor.DARK_BLUE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLUE).boldOff().toString());
replacements.put(ChatColor.DARK_GREEN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.GREEN).boldOff().toString());
replacements.put(ChatColor.DARK_AQUA, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.CYAN).boldOff().toString());
replacements.put(ChatColor.DARK_RED, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.RED).boldOff().toString());
replacements.put(ChatColor.DARK_PURPLE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.MAGENTA).boldOff().toString());
replacements.put(ChatColor.GOLD, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.YELLOW).boldOff().toString());
replacements.put(ChatColor.GRAY, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.WHITE).boldOff().toString());
replacements.put(ChatColor.DARK_GRAY, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLACK).bold().toString());
replacements.put(ChatColor.BLUE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.BLUE).bold().toString());
replacements.put(ChatColor.GREEN, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.GREEN).bold().toString());
replacements.put(ChatColor.AQUA, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.CYAN).bold().toString());
replacements.put(ChatColor.RED, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.RED).bold().toString());
replacements.put(ChatColor.LIGHT_PURPLE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.MAGENTA).bold().toString());
replacements.put(ChatColor.YELLOW, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.YELLOW).bold().toString());
replacements.put(ChatColor.WHITE, Ansi.ansi().a(Ansi.Attribute.RESET).fg(Ansi.Color.WHITE).bold().toString());
replacements.put(ChatColor.MAGIC, Ansi.ansi().a(Ansi.Attribute.BLINK_SLOW).toString());
replacements.put(ChatColor.BOLD, Ansi.ansi().a(Ansi.Attribute.UNDERLINE_DOUBLE).toString());
replacements.put(ChatColor.STRIKETHROUGH, Ansi.ansi().a(Ansi.Attribute.STRIKETHROUGH_ON).toString());
replacements.put(ChatColor.UNDERLINE, Ansi.ansi().a(Ansi.Attribute.UNDERLINE).toString());
replacements.put(ChatColor.ITALIC, Ansi.ansi().a(Ansi.Attribute.ITALIC).toString());
replacements.put(ChatColor.RESET, Ansi.ansi().a(Ansi.Attribute.RESET).toString());
String result = str;
for (ChatColor color : ChatColor.values())
{
if (replacements.containsKey(color))
{
result = result.replaceAll("(?i)" + color.toString(), replacements.get(color));
}
else
{
result = result.replaceAll("(?i)" + color.toString(), "");
}
}
result += Ansi.ansi().reset().toString();
return result;
}
public String format(LogRecord logrecord) {
try {
Class.forName("org.bukkit.craftbukkit.command.ColouredConsoleSender");
} catch (ClassNotFoundException ignored) {
// MEANS WE'RE ON PAPER/TACO/OTHER SHIT
colorize(logrecord);
}
StringBuilder stringbuilder = new StringBuilder();
stringbuilder.append(" [");
stringbuilder.append(this.dateFormat.format(logrecord.getMillis())).append(" ");
stringbuilder.append(logrecord.getLevel().getName()).append("]: ");
stringbuilder.append(this.formatMessage(logrecord));
stringbuilder.append('\n');
Throwable throwable = logrecord.getThrown();
if (throwable != null) {
StringWriter stringwriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringwriter));
stringbuilder.append(stringwriter.toString());
}
return stringbuilder.toString();
}
private void colorize(LogRecord logrecord)
{
String result = ColorizeString(logrecord.getMessage());
logrecord.setMessage(result);
}
}

View File

@@ -1,128 +1,182 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import com.ryanmichela.sshd.ConsoleCommandCompleter;
import com.ryanmichela.sshd.ConsoleLogFormatter;
import com.ryanmichela.sshd.FlushyOutputStream;
import com.ryanmichela.sshd.FlushyStreamHandler;
import com.ryanmichela.sshd.SshTerminal;
import com.ryanmichela.sshd.SshdPlugin;
import com.ryanmichela.sshd.StreamHandlerAppender;
import com.ryanmichela.sshd.implementations.SSHDCommandSender;
import com.ryanmichela.sshd.ConsoleLogFormatter;
import jline.console.ConsoleReader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger;
import org.apache.sshd.common.Factory; import org.apache.sshd.common.Factory;
import org.apache.sshd.server.Command; import org.apache.sshd.server.shell.ShellFactory;
import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.channel.ChannelSession;
import org.apache.sshd.server.Environment; import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.ExitCallback;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.libs.jline.console.ConsoleReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.logging.Formatter; import java.net.InetAddress;
import java.util.logging.Logger; import java.util.logging.Level;
import java.util.logging.StreamHandler; import java.util.logging.StreamHandler;
public class ConsoleShellFactory implements Factory<Command> { public class ConsoleShellFactory implements ShellFactory {
public Command create() { public Command createShell(ChannelSession cs) {
return new ConsoleShell(); return new ConsoleShell();
} }
public static class ConsoleShell implements Command, Runnable { public class ConsoleShell implements Command, Runnable {
private InputStream in; private InputStream in;
private OutputStream out; private OutputStream out;
private OutputStream err; private OutputStream err;
private ExitCallback callback; private ExitCallback callback;
private Environment environment; private Environment environment;
private Thread thread; private Thread thread;
private String Username;
StreamHandler streamHandler; StreamHandlerAppender streamHandlerAppender;
ConsoleReader consoleReader; public ConsoleReader ConsoleReader;
public SSHDCommandSender SshdCommandSender;
public InputStream getIn() { public InputStream getIn() {
return in; return in;
} }
public OutputStream getOut() { public OutputStream getOut() {
return out; return out;
} }
public OutputStream getErr() { public OutputStream getErr() {
return err; return err;
} }
public Environment getEnvironment() { public Environment getEnvironment() {
return environment; return environment;
} }
public void setInputStream(InputStream in) { public void setInputStream(InputStream in) {
this.in = in; this.in = in;
} }
public void setOutputStream(OutputStream out) { public void setOutputStream(OutputStream out) {
this.out = out; this.out = out;
} }
public void setErrorStream(OutputStream err) { public void setErrorStream(OutputStream err) {
this.err = err; this.err = err;
} }
public void setExitCallback(ExitCallback callback) { public void setExitCallback(ExitCallback callback) {
this.callback = callback; this.callback = callback;
} }
public void start(Environment env) throws IOException { @Override
public void start(ChannelSession cs, Environment env) throws IOException
{
try
{
this.ConsoleReader = new ConsoleReader(in, new FlushyOutputStream(out), new SshTerminal());
this.ConsoleReader.setExpandEvents(true);
this.ConsoleReader.addCompleter(new ConsoleCommandCompleter());
Formatter bukkitFormatter = Bukkit.getLogger().getHandlers()[0].getFormatter(); StreamHandler streamHandler = new FlushyStreamHandler(out, new ConsoleLogFormatter(), this.ConsoleReader);
streamHandlerAppender = new StreamHandlerAppender(streamHandler);
try { ((Logger)LogManager.getRootLogger()).addAppender(streamHandlerAppender);
consoleReader = new ConsoleReader(in, new FlushyOutputStream(out), new SshTerminal());
consoleReader.setExpandEvents(true);
consoleReader.addCompleter(new ConsoleCommandCompleter());
streamHandler = new FlushyStreamHandler(out, bukkitFormatter, consoleReader); this.environment = env;
Bukkit.getLogger().addHandler(streamHandler); this.Username = env.getEnv().get(Environment.ENV_USER);
Logger.getLogger("").addHandler(streamHandler); this.SshdCommandSender = new SSHDCommandSender();
this.SshdCommandSender.console = this;
thread = new Thread(this, "SSHD ConsoleShell " + this.Username);
thread.start();
}
catch (Exception e)
{
throw new IOException("Error starting shell", e);
}
}
environment = env; @Override
thread = new Thread(this, "EchoShell " + env.getEnv().get(Environment.ENV_USER)); public void destroy(ChannelSession cs) { ((Logger)LogManager.getRootLogger()).removeAppender(streamHandlerAppender); }
thread.start();
} catch (Exception e) {
throw new IOException("Error starting shell", e);
}
}
public void destroy() { public void run()
Bukkit.getLogger().removeHandler(streamHandler); {
Logger.getLogger("").removeHandler(streamHandler); try
} {
if (!SshdPlugin.instance.getConfig().getString("Mode").equals("RPC"))
printPreamble(this.ConsoleReader);
while (true)
{
String command = this.ConsoleReader.readLine("\r>", null);
// The user sent CTRL+D to close the shell, terminate the session.
if (command == null)
break;
// Skip someone spamming enter
if (command.trim().isEmpty())
continue;
// User wants to exit
if (command.equals("exit") || command.equals("quit"))
break;
// Clear the text from the screen (on supported terminals)
if (command.equals("cls"))
{
this.ConsoleReader.clearScreen();
continue;
}
public void run() { Bukkit.getScheduler().runTask(
String command; SshdPlugin.instance, () ->
try { {
printPreamble(consoleReader); if (SshdPlugin.instance.getConfig().getString("Mode").equals("RPC") && command.startsWith("rpc"))
while(true) { {
command = consoleReader.readLine("\r>", null); // NO ECHO NO PREAMBLE AND SHIT
if (command != null) { String cmd = command.substring("rpc".length() + 1, command.length());
if (command.equals("exit")) { Bukkit.dispatchCommand(this.SshdCommandSender, cmd);
break; }
} else
SshdPlugin.instance.getLogger().info("[U: " + environment.getEnv().get(Environment.ENV_USER) + "] " + command); {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); SshdPlugin.instance.getLogger().info("<" + this.Username + "> " + command);
} Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
} }
} catch (IOException e) { });
SshdPlugin.instance.getLogger().severe("Error processing command from SSH"); }
} finally { }
callback.onExit(0); catch (IOException e)
} {
} SshdPlugin.instance.getLogger().log(Level.SEVERE, "Error processing command from SSH", e);
}
finally
{
SshdPlugin.instance.getLogger().log(Level.INFO, this.Username + " disconnected from SSH.");
callback.onExit(0);
}
}
private void printPreamble(ConsoleReader consoleReader) throws IOException{ private void printPreamble(ConsoleReader cr) throws IOException
consoleReader.println(" _____ _____ _ _ _____"); {
consoleReader.println(" / ____/ ____| | | | __ \\"); cr.println(" _____ _____ _ _ _____" + "\r");
consoleReader.println("| (___| (___ | |__| | | | |"); cr.println(" / ____/ ____| | | | __ \\" + "\r");
consoleReader.println(" \\___ \\\\___ \\| __ | | | |"); cr.println("| (___| (___ | |__| | | | |" + "\r");
consoleReader.println(" ____) |___) | | | | |__| |"); cr.println(" \\___ \\\\___ \\| __ | | | |" + "\r");
consoleReader.println("|_____/_____/|_| |_|_____/"); cr.println(" ____) |___) | | | | |__| |" + "\r");
consoleReader.println("Connected to: " + Bukkit.getServer().getName()); cr.println("|_____/_____/|_| |_|_____/" + "\r");
consoleReader.println("- " + Bukkit.getServer().getMotd()); // Doesn't really guarantee our actual system hostname but
consoleReader.println(); // it's better than not having one at all.
consoleReader.println("Type 'exit' to exit the shell."); cr.println("Connected to: " + InetAddress.getLocalHost().getHostName() + " (" + Bukkit.getServer().getName() + ")\r");
consoleReader.println("==============================================="); cr.println(ConsoleLogFormatter.ColorizeString(Bukkit.getServer().getMotd()) + "\r");
} cr.println("\r");
} cr.println("Type 'exit' to exit the shell." + "\r");
cr.println("===============================================" + "\r");
}
}
} }

View File

@@ -1,5 +1,7 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.sshd.common.SshException;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@@ -7,7 +9,9 @@ import java.io.OutputStream;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class FlushyOutputStream extends OutputStream { public class FlushyOutputStream extends OutputStream {
private OutputStream base; private OutputStream base;
private boolean isClosed = false;
public FlushyOutputStream(OutputStream base) { public FlushyOutputStream(OutputStream base) {
this.base = base; this.base = base;
@@ -15,19 +19,31 @@ public class FlushyOutputStream extends OutputStream {
@Override @Override
public void write(int b) throws IOException { public void write(int b) throws IOException {
if (isClosed) return;
base.write(b); base.write(b);
base.flush(); base.flush();
} }
@Override @Override
public void write(byte[] b) throws IOException { public void write(byte[] b) throws IOException {
if (isClosed) return;
base.write(b); base.write(b);
base.flush(); base.flush();
} }
@Override @Override
public void write(byte[] b, int off, int len) throws IOException { public void write(byte[] b, int off, int len) throws IOException {
base.write(b, off, len); if (isClosed) return;
base.flush(); try {
base.write(b, off, len);
base.flush();
} catch (SshException e) {
if (!e.getMessage().contains("channel already closed")) throw e;
}
}
@Override
public void close() {
isClosed = true;
} }
} }

View File

@@ -1,7 +1,7 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import jline.console.ConsoleReader;
import org.apache.sshd.common.SshException; import org.apache.sshd.common.SshException;
import org.bukkit.craftbukkit.libs.jline.console.ConsoleReader;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@@ -11,6 +11,7 @@ import java.util.logging.*;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class FlushyStreamHandler extends StreamHandler { public class FlushyStreamHandler extends StreamHandler {
private ConsoleReader reader; private ConsoleReader reader;
public FlushyStreamHandler(OutputStream out, Formatter formatter, ConsoleReader reader) { public FlushyStreamHandler(OutputStream out, Formatter formatter, ConsoleReader reader) {

View File

@@ -1,97 +0,0 @@
package com.ryanmichela.sshd;
import org.apache.mina.util.Base64;
import java.io.Reader;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.RSAPublicKeySpec;
/**
* Copyright 2013 Ryan Michela
*/
public class PemDecoder extends java.io.BufferedReader {
private static final String BEGIN = "^-+\\s*BEGIN.+";
private static final String END = "^-+\\s*END.+";
private static final String COMMENT = "Comment:";
public PemDecoder(Reader in) {
super(in);
}
public PublicKey getPemBytes() throws Exception {
StringBuffer b64 = new StringBuffer();
String line = readLine();
if (!line.matches(BEGIN)) {
return null;
}
for(line = readLine(); line != null; line = readLine()) {
if (!line.matches(END) && !line.startsWith(COMMENT)) {
b64.append(line.trim());
}
}
return decodePublicKey(b64.toString());
}
private byte[] bytes;
private int pos;
private PublicKey decodePublicKey(String keyLine) throws Exception {
bytes = null;
pos = 0;
// look for the Base64 encoded part of the line to decode
// both ssh-rsa and ssh-dss begin with "AAAA" due to the length bytes
for (String part : keyLine.split(" ")) {
if (part.startsWith("AAAA")) {
bytes = Base64.decodeBase64(part.getBytes());
break;
}
}
if (bytes == null) {
throw new IllegalArgumentException("no Base64 part to decode");
}
String type = decodeType();
if (type.equals("ssh-rsa")) {
BigInteger e = decodeBigInt();
BigInteger m = decodeBigInt();
RSAPublicKeySpec spec = new RSAPublicKeySpec(m, e);
return KeyFactory.getInstance("RSA").generatePublic(spec);
} else if (type.equals("ssh-dss")) {
BigInteger p = decodeBigInt();
BigInteger q = decodeBigInt();
BigInteger g = decodeBigInt();
BigInteger y = decodeBigInt();
DSAPublicKeySpec spec = new DSAPublicKeySpec(y, p, q, g);
return KeyFactory.getInstance("DSA").generatePublic(spec);
} else {
throw new IllegalArgumentException("unknown type " + type);
}
}
private String decodeType() {
int len = decodeInt();
String type = new String(bytes, pos, len);
pos += len;
return type;
}
private int decodeInt() {
return ((bytes[pos++] & 0xFF) << 24) | ((bytes[pos++] & 0xFF) << 16)
| ((bytes[pos++] & 0xFF) << 8) | (bytes[pos++] & 0xFF);
}
private BigInteger decodeBigInt() {
int len = decodeInt();
byte[] bigIntBytes = new byte[len];
System.arraycopy(bytes, pos, bigIntBytes, 0, len);
pos += len;
return new BigInteger(bigIntBytes);
}
}

View File

@@ -1,50 +1,82 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang.ArrayUtils;
import org.apache.sshd.server.PublickeyAuthenticator; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.session.ServerSession;
import java.io.File; import java.io.File;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.io.FileReader; import java.io.FileReader;
import java.security.PublicKey; import java.security.PublicKey;
/** /**
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class PublicKeyAuthenticator implements PublickeyAuthenticator { public class PublicKeyAuthenticator implements PublickeyAuthenticator
private File authorizedKeysDir; {
private File authorizedKeysDir;
private Map<String, Integer> FailCounts = new HashMap<String, Integer>();
public PublicKeyAuthenticator(File authorizedKeysDir) { public PublicKeyAuthenticator(File authorizedKeysDir) { this.authorizedKeysDir = authorizedKeysDir; }
this.authorizedKeysDir = authorizedKeysDir;
}
@Override @Override public boolean authenticate(String username, PublicKey key, ServerSession session)
public boolean authenticate(String username, PublicKey key, ServerSession session) { {
byte[] keyBytes = key.getEncoded(); byte[] keyBytes = key.getEncoded();
File keyFile = new File(authorizedKeysDir, username); File keyFile = new File(authorizedKeysDir, username);
Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries");
if (keyFile.exists()) { if (keyFile.exists())
try { {
try
{
// Read all the public key entries
List<AuthorizedKeyEntry> pklist = AuthorizedKeyEntry.readAuthorizedKeys(keyFile.toPath());
// Get an authenticator
PublickeyAuthenticator auth = PublickeyAuthenticator.fromAuthorizedEntries(username, session, pklist,
PublicKeyEntryResolver.IGNORING);
FileReader fr = new FileReader(keyFile); // Validate that the logging in user has the same valid SSH key
PemDecoder pd = new PemDecoder(fr); if (auth.authenticate(username, key, session))
PublicKey k = pd.getPemBytes(); {
pd.close(); FailCounts.put(username, 0);
return true;
}
else
{
SshdPlugin.instance.getLogger().info(
username + " failed authentication via SSH session using key file " + keyFile.getAbsolutePath());
}
if (k != null) { // If the user fails with several SSH keys, then terminate the connection.
if (ArrayUtils.isEquals(key.getEncoded(), k.getEncoded())) { if (this.FailCounts.containsKey(username))
return true; this.FailCounts.put(username, this.FailCounts.get(username) + 1);
} else
} else { this.FailCounts.put(username, 1);
SshdPlugin.instance.getLogger().severe("Failed to parse PEM file. " + keyFile.getAbsolutePath());
}
} catch (Exception e) {
SshdPlugin.instance.getLogger().severe("Failed to process public key " + keyFile.getAbsolutePath() + ". " + e.getMessage());
}
} else {
SshdPlugin.instance.getLogger().warning("Could not locate public key for " + username + ". Make sure the user's key is named the same as their user name without a file extension.");
}
return false; if (this.FailCounts.get(username) >= tries)
} {
this.FailCounts.put(username, 0);
session.close(true);
}
return false;
}
catch (Exception e)
{
SshdPlugin.instance.getLogger().severe("Failed to process public key " + keyFile.getAbsolutePath() + " " + e.getMessage());
}
}
else
{
SshdPlugin.instance.getLogger().warning("Could not locate public key for " + username
+ ". Make sure the user's key is named the same as their user name "
+ "without a file extension.");
}
return false;
}
} }

View File

@@ -8,6 +8,7 @@ import java.lang.reflect.Modifier;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class ReflectionUtil { public class ReflectionUtil {
public static void setProtectedValue(Object o, String field, Object newValue) { public static void setProtectedValue(Object o, String field, Object newValue) {
setProtectedValue(o.getClass(), o, field, newValue); setProtectedValue(o.getClass(), o, field, newValue);
} }
@@ -28,9 +29,7 @@ public class ReflectionUtil {
modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
f.set(o, newValue); f.set(o, newValue);
} catch (NoSuchFieldException ex) { } catch (NoSuchFieldException | IllegalAccessException ex) {
System.out.println("*** " + c.getName() + ":" + ex);
} catch (IllegalAccessException ex) {
System.out.println("*** " + c.getName() + ":" + ex); System.out.println("*** " + c.getName() + ":" + ex);
} }
} }
@@ -38,9 +37,9 @@ public class ReflectionUtil {
public static <T> T getProtectedValue(Object obj, String fieldName) { public static <T> T getProtectedValue(Object obj, String fieldName) {
try { try {
Class c = obj.getClass(); Class c = obj.getClass();
while(c != Object.class) { while (c != Object.class) {
Field[] fields = c.getDeclaredFields(); Field[] fields = c.getDeclaredFields();
for(Field f : fields) { for (Field f : fields) {
if (f.getName() == fieldName) { if (f.getName() == fieldName) {
f.setAccessible(true); f.setAccessible(true);
return (T) f.get(obj); return (T) f.get(obj);
@@ -78,7 +77,7 @@ public class ReflectionUtil {
public static Object invokeProtectedMethod(Class c, Object o, String method, Object... args) { public static Object invokeProtectedMethod(Class c, Object o, String method, Object... args) {
try { try {
Class[] pTypes = new Class[args.length]; Class[] pTypes = new Class[args.length];
for(int i = 0; i < args.length; i++) { for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Integer) { if (args[i] instanceof Integer) {
pTypes[i] = int.class; pTypes[i] = int.class;
} else { } else {
@@ -89,8 +88,7 @@ public class ReflectionUtil {
Method m = c.getDeclaredMethod(method, pTypes); Method m = c.getDeclaredMethod(method, pTypes);
m.setAccessible(true); m.setAccessible(true);
return m.invoke(o, args); return m.invoke(o, args);
} } catch (Exception ex) {
catch (Exception ex) {
System.out.println("*** " + c.getName() + "." + method + "(): " + ex); System.out.println("*** " + c.getName() + "." + method + "(): " + ex);
return null; return null;
} }

View File

@@ -1,19 +1,19 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.bukkit.craftbukkit.libs.jline.TerminalSupport; import jline.TerminalSupport;
/** /**
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class SshTerminal extends TerminalSupport { public class SshTerminal extends TerminalSupport {
protected SshTerminal() { protected SshTerminal() {
super(true); super(true);
} }
@Override @Override
public void init() throws Exception { public void init() throws Exception {
setAnsiSupported(true); setAnsiSupported(true);
setEchoEnabled(true); setEchoEnabled(true);
} }
} }

View File

@@ -1,62 +1,88 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.sshd.SshServer; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import com.ryanmichela.sshd.ConsoleShellFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileSystems;
import java.util.Collections;
import java.util.logging.Level; import java.util.logging.Level;
/** /**
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public class SshdPlugin extends JavaPlugin { public
private SshServer sshd; class SshdPlugin extends JavaPlugin
public static SshdPlugin instance; {
@Override private SshServer sshd;
public void onLoad() { public static SshdPlugin instance;
saveDefaultConfig();
File authorizedKeys = new File(getDataFolder(), "authorized_keys");
if (!authorizedKeys.exists()) {
authorizedKeys.mkdirs();
}
// Don't go any lower than INFO or SSHD will cause a stack overflow exception. @Override public void onLoad()
// SSHD will log that it wrote bites to the output stream, which writes {
// bytes to the output stream - ad nauseaum. saveDefaultConfig();
getLogger().setLevel(Level.INFO); File authorizedKeys = new File(getDataFolder(), "authorized_keys");
} if (!authorizedKeys.exists())
{
authorizedKeys.mkdirs();
}
@Override // Don't go any lower than INFO or SSHD will cause a stack overflow exception.
public void onEnable() { // SSHD will log that it wrote bites to the output stream, which writes
instance = this; // bytes to the output stream - ad nauseaum.
getLogger().setLevel(Level.INFO);
}
sshd = SshServer.setUpDefaultServer(); @Override public void onEnable()
sshd.setPort(getConfig().getInt("port", 22)); {
instance = this;
File hostKey = new File(getDataFolder(), "hostkey"); sshd = SshServer.setUpDefaultServer();
File authorizedKeys = new File(getDataFolder(), "authorized_keys"); sshd.setPort(getConfig().getInt("Port", 1025));
String host = getConfig().getString("ListenAddress", "all");
sshd.setHost(host.equals("all") ? null : host);
sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKey.getPath())); File hostKey = new File(getDataFolder(), "hostkey");
sshd.setShellFactory(new ConsoleShellFactory()); File authorizedKeys = new File(getDataFolder(), "authorized_keys");
sshd.setPasswordAuthenticator(new ConfigPasswordAuthenticator());
sshd.setPublickeyAuthenticator(new PublicKeyAuthenticator(authorizedKeys));
sshd.setCommandFactory(new ConsoleCommandFactory());
try {
sshd.start();
} catch (IOException e) {
getLogger().severe("Failed to start SSH server! " + e.getMessage());
}
}
@Override sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKey.toPath()));
public void onDisable() { sshd.setShellFactory(new ConsoleShellFactory());
try { sshd.setPasswordAuthenticator(new ConfigPasswordAuthenticator());
sshd.stop(); sshd.setPublickeyAuthenticator(new PublicKeyAuthenticator(authorizedKeys));
} catch (InterruptedException e) {
// do nothing if (getConfig().getBoolean("EnableSFTP"))
} {
} sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
sshd.setFileSystemFactory(
new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent()));
}
sshd.setCommandFactory(new ConsoleCommandFactory());
try
{
sshd.start();
}
catch (IOException e)
{
getLogger().log(Level.SEVERE, "Failed to start SSH server! ", e);
}
}
@Override public void onDisable()
{
try
{
sshd.stop();
}
catch (Exception e)
{
// do nothing
}
}
} }

View File

@@ -0,0 +1,92 @@
package com.ryanmichela.sshd;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.ErrorHandler;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import java.io.Serializable;
import java.util.UUID;
import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;
/**
* Copyright 2014 Ryan Michela
*/
public class StreamHandlerAppender implements Appender {
private StreamHandler streamHandler;
private UUID uuid;
public StreamHandlerAppender(StreamHandler streamHandler) {
this.streamHandler = streamHandler;
uuid = UUID.randomUUID();
}
@Override
public void append(LogEvent logEvent) {
java.util.logging.Level level;
if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.DEBUG)) {
level = java.util.logging.Level.FINE;
} else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.INFO)) {
level = java.util.logging.Level.INFO;
} else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.WARN)) {
level = java.util.logging.Level.WARNING;
} else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.ERROR)) {
level = java.util.logging.Level.SEVERE;
} else {
level = java.util.logging.Level.INFO;
}
String message = logEvent.getMessage().getFormattedMessage();
LogRecord logRecord = new LogRecord(level, message);
streamHandler.publish(logRecord);
}
@Override
public String getName() {
return "StreamHandlerAppender:" + uuid.toString();
}
@Override
public Layout<? extends Serializable> getLayout() {
return null;
}
@Override
public boolean ignoreExceptions() {
return false;
}
@Override
public ErrorHandler getHandler() {
return null;
}
@Override
public void setHandler(ErrorHandler errorHandler) {
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public boolean isStarted() {
return true;
}
@Override
public boolean isStopped() {
return false;
}
}

View File

@@ -6,11 +6,13 @@ import java.util.concurrent.ExecutionException;
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
public abstract class Waitable<T> implements Runnable { public abstract class Waitable<T> implements Runnable {
private enum Status { private enum Status {
WAITING, WAITING,
RUNNING, RUNNING,
FINISHED, FINISHED,
} }
Throwable t = null; Throwable t = null;
T value = null; T value = null;
Status status = Status.WAITING; Status status = Status.WAITING;

View File

@@ -0,0 +1,139 @@
package com.ryanmichela.sshd.implementations;
import com.ryanmichela.sshd.SshdPlugin;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.conversations.ManuallyAbandonedConversationCanceller;
import org.bukkit.permissions.PermissibleBase;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin;
import com.ryanmichela.sshd.ConsoleShellFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import java.util.logging.Level;
public class SSHDCommandSender implements ConsoleCommandSender, CommandSender {
private final PermissibleBase perm = new PermissibleBase(this);
private final SSHDConversationTracker conversationTracker = new SSHDConversationTracker();
// Set by the upstream allocating function
public ConsoleShellFactory.ConsoleShell console;
public void sendMessage(String message) {
this.sendRawMessage(message);
}
public void sendRawMessage(String message)
{
// What the fuck does this code even do? Are we sending to one client or all of them?
if (this.console.ConsoleReader == null)
return;
try
{
this.console.ConsoleReader.println(ChatColor.stripColor(message));
}
catch (IOException e)
{
SshdPlugin.instance.getLogger().log(Level.SEVERE, "Error sending message to SSHDCommandSender", e);
}
}
public void sendMessage(String[] messages) {
Arrays.asList(messages).forEach(this::sendMessage);
}
public String getName() {
return "SSHD Console";
}
public boolean isOp() {
return true;
}
public void setOp(boolean value) {
throw new UnsupportedOperationException("Cannot change operator status of server console");
}
public boolean beginConversation(Conversation conversation) {
return this.conversationTracker.beginConversation(conversation);
}
public void abandonConversation(Conversation conversation) {
this.conversationTracker.abandonConversation(conversation, new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller()));
}
public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) {
this.conversationTracker.abandonConversation(conversation, details);
}
public void acceptConversationInput(String input) {
this.conversationTracker.acceptConversationInput(input);
}
public boolean isConversing() {
return this.conversationTracker.isConversing();
}
public boolean isPermissionSet(String name) {
return this.perm.isPermissionSet(name);
}
public boolean isPermissionSet(Permission perm) {
return this.perm.isPermissionSet(perm);
}
public boolean hasPermission(String name) {
return this.perm.hasPermission(name);
}
public boolean hasPermission(Permission perm) {
return this.perm.hasPermission(perm);
}
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
return this.perm.addAttachment(plugin, name, value);
}
public PermissionAttachment addAttachment(Plugin plugin) {
return this.perm.addAttachment(plugin);
}
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
return this.perm.addAttachment(plugin, name, value, ticks);
}
public PermissionAttachment addAttachment(Plugin plugin, int ticks) {
return this.perm.addAttachment(plugin, ticks);
}
public void removeAttachment(PermissionAttachment attachment) {
this.perm.removeAttachment(attachment);
}
public void recalculatePermissions() {
this.perm.recalculatePermissions();
}
public Set<PermissionAttachmentInfo> getEffectivePermissions() {
return this.perm.getEffectivePermissions();
}
public boolean isPlayer() {
return false;
}
public Server getServer() {
return Bukkit.getServer();
}
}

View File

@@ -0,0 +1,78 @@
package com.ryanmichela.sshd.implementations;
import org.bukkit.Bukkit;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.conversations.ManuallyAbandonedConversationCanceller;
import java.util.LinkedList;
import java.util.logging.Level;
public class SSHDConversationTracker {
private LinkedList<Conversation> conversationQueue = new LinkedList<>();
synchronized boolean beginConversation(Conversation conversation) {
if (!this.conversationQueue.contains(conversation)) {
this.conversationQueue.addLast(conversation);
if (this.conversationQueue.getFirst() == conversation) {
conversation.begin();
conversation.outputNextPrompt();
return true;
}
}
return true;
}
synchronized void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) {
if (!this.conversationQueue.isEmpty()) {
if (this.conversationQueue.getFirst() == conversation) {
conversation.abandon(details);
}
if (this.conversationQueue.contains(conversation)) {
this.conversationQueue.remove(conversation);
}
if (!this.conversationQueue.isEmpty()) {
this.conversationQueue.getFirst().outputNextPrompt();
}
}
}
public synchronized void abandonAllConversations() {
LinkedList<Conversation> oldQueue = this.conversationQueue;
this.conversationQueue = new LinkedList<>();
for (Conversation conversation : oldQueue) {
try {
conversation.abandon(new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller()));
} catch (Throwable var5) {
Bukkit.getLogger().log(Level.SEVERE, "Unexpected exception while abandoning a conversation", var5);
}
}
}
synchronized void acceptConversationInput(String input) {
if (this.isConversing()) {
Conversation conversation = this.conversationQueue.getFirst();
try {
conversation.acceptInput(input);
} catch (Throwable var4) {
conversation.getContext().getPlugin().getLogger().log(Level.WARNING, String.format("Plugin %s generated an exception whilst handling conversation input", conversation.getContext().getPlugin().getDescription().getFullName()), var4);
}
}
}
synchronized boolean isConversing() {
return !this.conversationQueue.isEmpty();
}
public synchronized boolean isConversingModaly() {
return this.isConversing() && this.conversationQueue.getFirst().isModal();
}
}

View File

@@ -22,10 +22,7 @@ public class PluginSlf4jFactory implements ILoggerFactory {
private String name; private String name;
private boolean isEnabled(Level level) { private boolean isEnabled(Level level) {
if (SshdPlugin.instance != null) { return SshdPlugin.instance != null && SshdPlugin.instance.getLogger().isLoggable(level);
return SshdPlugin.instance.getLogger().isLoggable(level);
}
return false;
} }
private void log(Level level, String s, Object[] objects) { private void log(Level level, String s, Object[] objects) {

View File

@@ -1,12 +1,35 @@
# This is the port the SSH server will listen on. # The IP addresses(s) the SSH server will listen on. Use a comma separated list for multiple addresses.
port: 22 # Leave as "all" for all addresses.
ListenAddress: all
# The port the SSH server will listen on. Note that anything above 1024 will require you to run
# the whole minecraft server with elevated privileges, this is not recommended and you should
# use iptables to route packets from a lower port.
Port: 1025
# Operational mode. Don't touch if you don't know what you're doing. Can be either DEFAULT or RPC
Mode: DEFAULT
# Enable built-in SFTP server or not. You'll be able to connect and upload/download files via SFTP protocol.
# Might be useful for testing purposes as well , i. e. docker containers.
EnableSFTP: true
# Number of times a person can fail to use an SSH key or enter a password
# before it terminates the connection.
LoginRetries: 3
# By default, only public key authentication is enabled. This is the most secure mode. # By default, only public key authentication is enabled. This is the most secure mode.
# To authorize a user to log in with public key authentication, install their public # To authorize a user to login with their public key, install their key using the
# PEM certificate in the authorized_users directory. Name the key file with user's user # OpenSSH authorized_keys file format in the authorized_users directory. Name the key
# name (no file extension). # file with the user's username and no extension. Note: If you want to let a user have
# many keys, you can append the keys to their file in authorized_users.
# For less secure username and password based authentication, complete the sections below. # For less secure username and password based authentication, complete the sections below.
credentials:
# Type of hashing to use for the passwords below.
# Options are: PLAIN (insecure), bcrypt, pbkdf, sha256
PasswordType: bcrypt
# Associate each username with a password hash (or the password if the PasswordType is set to PLAIN)
Credentials:
# user1: password1 # user1: password1
# user2: password2 # user2: password2

View File

@@ -1,4 +1,4 @@
name: SSHD name: SSHD
version: "1.0" version: ${project.version}
author: Ryan Michela author: Ryan Michela, Haarolean, toxuin, Justin Crawford
main: com.ryanmichela.sshd.SshdPlugin main: com.ryanmichela.sshd.SshdPlugin