Compare commits

...

13 Commits

Author SHA1 Message Date
21364a3f63
refactor some of the packages, update licenesing 2023-03-17 21:47:17 +04:00
a0cafa3e62
Limework fork is born! 2023-03-17 20:43:59 +04:00
Justin Crawford
d348e26b49
Fixed a bug where sometimes sessions are disconnected due to some hosts not resolving localhost properly 2020-04-06 06:06:47 -07:00
Justin Crawford
e0080fb1a0
Added hover text and click to copy for /mkpasswd
- Added hover text and click to copy so you can now copy your hash
to the clipboard.
- Added default values for the configuration in case none were set for some reason.
- Validate permissions from the config (something I forgot to do)
- Tell the user permission denied if they don't have permission to run the command.
- Added a default pseudo-user for global permissions and rules.
- Updated some verbage in the config comments
2020-03-26 21:50:39 -07:00
Zachery
40f63bdeeb
Added Discord to readme 2020-02-06 12:03:21 -06:00
Justin Crawford
5a247576c4
Formatting 2019-11-23 21:53:58 -08:00
Justin Crawford
ae8d5bff1d
Implemented access control for SFTP 2019-11-23 01:49:27 -08:00
Zachery
3ad2a810f8
Update MkpasswdCommand.java 2019-10-16 00:47:53 -05:00
Zachery
de9d723416
Update README.md 2019-10-16 00:42:09 -05:00
Justin Crawford
1c93b9fc0e
Increment project version for sponge release 2019-10-13 01:03:48 -07:00
Justin Crawford
170a96eb94
Update mkpasswd to be slightly more secure
Try to ensure that the mkpasswd command run in ssh sessions only echos to
ssh client running that command. This gives us slightly more security
against other session users seeing the hashed password.

Fixed console sending with some of the APIs

Updated version to 1.3.7 to match for next spigot + bungeecord release.
2019-10-10 19:48:32 -07:00
Justin Crawford
ec1ed8cb9e
Add new logo 2019-10-09 00:07:16 -07:00
Zachery
67146ce589
Change the load method to startup
This lets the plugin start before the world is generated, this is done so you can SSH into the server while it's still technically starting.
2019-10-08 17:37:54 -05:00
34 changed files with 1161 additions and 635 deletions

View File

@ -1,68 +0,0 @@
---
#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'
...

View File

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

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ryanmichela</groupId>
<artifactId>sshd</artifactId>
<version>2.1.0</version>
</parent>
<artifactId>Minecraft-SSHD-Bukkit</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- License: This module extends bukkit which is GPL v2 -->
<licenses>
<license>
<name>GPL2</name>
<url>http://www.gnu.org/licenses/gpl-2.0.html</url>
</license>
</licenses>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- 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>
<include>motd.txt</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>${project.name}-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<!-- Compile plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -61,7 +61,8 @@ import java.security.SecureRandom;
* @author Damien Miller * @author Damien Miller
* @version 0.2 * @version 0.2
*/ */
public class BCrypt { public class BCrypt
{
// BCrypt parameters // BCrypt parameters
private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;
private static final int BCRYPT_SALT_LEN = 16; private static final int BCRYPT_SALT_LEN = 16;
@ -383,8 +384,8 @@ public class BCrypt {
* @return base64-encoded string * @return base64-encoded string
* @exception IllegalArgumentException if the length is invalid * @exception IllegalArgumentException if the length is invalid
*/ */
private static String encode_base64(byte d[], int len) private static String encode_base64(byte d[], int len) throws IllegalArgumentException
throws IllegalArgumentException { {
int off = 0; int off = 0;
StringBuffer rs = new StringBuffer(); StringBuffer rs = new StringBuffer();
int c1, c2; int c1, c2;
@ -392,19 +393,23 @@ public class BCrypt {
if (len <= 0 || len > d.length) if (len <= 0 || len > d.length)
throw new IllegalArgumentException ("Invalid len"); throw new IllegalArgumentException ("Invalid len");
while (off < len) { while (off < len)
{
c1 = d[off++] & 0xff; c1 = d[off++] & 0xff;
rs.append(base64_code[(c1 >> 2) & 0x3f]); rs.append(base64_code[(c1 >> 2) & 0x3f]);
c1 = (c1 & 0x03) << 4; c1 = (c1 & 0x03) << 4;
if (off >= len) { if (off >= len)
{
rs.append(base64_code[c1 & 0x3f]); rs.append(base64_code[c1 & 0x3f]);
break; break;
} }
c2 = d[off++] & 0xff; c2 = d[off++] & 0xff;
c1 |= (c2 >> 4) & 0x0f; c1 |= (c2 >> 4) & 0x0f;
rs.append(base64_code[c1 & 0x3f]); rs.append(base64_code[c1 & 0x3f]);
c1 = (c2 & 0x0f) << 2; c1 = (c2 & 0x0f) << 2;
if (off >= len) { if (off >= len)
{
rs.append(base64_code[c1 & 0x3f]); rs.append(base64_code[c1 & 0x3f]);
break; break;
} }
@ -422,9 +427,11 @@ public class BCrypt {
* @param x the base64-encoded value * @param x the base64-encoded value
* @return the decoded value of x * @return the decoded value of x
*/ */
private static byte char64(char x) { private static byte char64(char x)
{
if ((int)x < 0 || (int)x > index_64.length) if ((int)x < 0 || (int)x > index_64.length)
return -1; return -1;
return index_64[(int)x]; return index_64[(int)x];
} }
@ -437,8 +444,8 @@ public class BCrypt {
* @return an array containing the decoded bytes * @return an array containing the decoded bytes
* @throws IllegalArgumentException if maxolen is invalid * @throws IllegalArgumentException if maxolen is invalid
*/ */
private static byte[] decode_base64(String s, int maxolen) private static byte[] decode_base64(String s, int maxolen) throws IllegalArgumentException
throws IllegalArgumentException { {
StringBuffer rs = new StringBuffer(); StringBuffer rs = new StringBuffer();
int off = 0, slen = s.length(), olen = 0; int off = 0, slen = s.length(), olen = 0;
byte ret[]; byte ret[];
@ -447,7 +454,8 @@ public class BCrypt {
if (maxolen <= 0) if (maxolen <= 0)
throw new IllegalArgumentException ("Invalid maxolen"); throw new IllegalArgumentException ("Invalid maxolen");
while (off < slen - 1 && olen < maxolen) { while (off < slen - 1 && olen < maxolen)
{
c1 = char64(s.charAt(off++)); c1 = char64(s.charAt(off++));
c2 = char64(s.charAt(off++)); c2 = char64(s.charAt(off++));
if (c1 == -1 || c2 == -1) if (c1 == -1 || c2 == -1)
@ -475,6 +483,7 @@ public class BCrypt {
ret = new byte[olen]; ret = new byte[olen];
for (off = 0; off < olen; off++) for (off = 0; off < olen; off++)
ret[off] = (byte)rs.charAt(off); ret[off] = (byte)rs.charAt(off);
return ret; return ret;
} }
@ -484,11 +493,13 @@ public class BCrypt {
* @param lr an array containing the two 32-bit half blocks * @param lr an array containing the two 32-bit half blocks
* @param off the position in the array of the blocks * @param off the position in the array of the blocks
*/ */
private final void encipher(int lr[], int off) { private final void encipher(int lr[], int off)
{
int i, n, l = lr[off], r = lr[off + 1]; int i, n, l = lr[off], r = lr[off + 1];
l ^= P[0]; l ^= P[0];
for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;)
{
// Feistel substitution on left word // Feistel substitution on left word
n = S[(l >> 24) & 0xff]; n = S[(l >> 24) & 0xff];
n += S[0x100 | ((l >> 16) & 0xff)]; n += S[0x100 | ((l >> 16) & 0xff)];
@ -514,12 +525,14 @@ public class BCrypt {
* current offset into data * current offset into data
* @return the next word of material from data * @return the next word of material from data
*/ */
private static int streamtoword(byte data[], int offp[]) { private static int streamtoword(byte data[], int offp[])
{
int i; int i;
int word = 0; int word = 0;
int off = offp[0]; int off = offp[0];
for (i = 0; i < 4; i++) { for (i = 0; i < 4; i++)
{
word = (word << 8) | (data[off] & 0xff); word = (word << 8) | (data[off] & 0xff);
off = (off + 1) % data.length; off = (off + 1) % data.length;
} }
@ -531,7 +544,8 @@ public class BCrypt {
/** /**
* Initialise the Blowfish key schedule * Initialise the Blowfish key schedule
*/ */
private void init_key() { private void init_key()
{
P = (int[])P_orig.clone(); P = (int[])P_orig.clone();
S = (int[])S_orig.clone(); S = (int[])S_orig.clone();
} }
@ -540,7 +554,8 @@ public class BCrypt {
* Key the Blowfish cipher * Key the Blowfish cipher
* @param key an array containing the key * @param key an array containing the key
*/ */
private void key(byte key[]) { private void key(byte key[])
{
int i; int i;
int koffp[] = { 0 }; int koffp[] = { 0 };
int lr[] = { 0, 0 }; int lr[] = { 0, 0 };
@ -549,13 +564,15 @@ public class BCrypt {
for (i = 0; i < plen; i++) for (i = 0; i < plen; i++)
P[i] = P[i] ^ streamtoword(key, koffp); P[i] = P[i] ^ streamtoword(key, koffp);
for (i = 0; i < plen; i += 2) { for (i = 0; i < plen; i += 2)
{
encipher(lr, 0); encipher(lr, 0);
P[i] = lr[0]; P[i] = lr[0];
P[i + 1] = lr[1]; P[i + 1] = lr[1];
} }
for (i = 0; i < slen; i += 2) { for (i = 0; i < slen; i += 2)
{
encipher(lr, 0); encipher(lr, 0);
S[i] = lr[0]; S[i] = lr[0];
S[i + 1] = lr[1]; S[i + 1] = lr[1];
@ -569,7 +586,8 @@ public class BCrypt {
* @param data salt information * @param data salt information
* @param key password information * @param key password information
*/ */
private void ekskey(byte data[], byte key[]) { private void ekskey(byte data[], byte key[])
{
int i; int i;
int koffp[] = { 0 }, doffp[] = { 0 }; int koffp[] = { 0 }, doffp[] = { 0 };
int lr[] = { 0, 0 }; int lr[] = { 0, 0 };
@ -578,7 +596,8 @@ public class BCrypt {
for (i = 0; i < plen; i++) for (i = 0; i < plen; i++)
P[i] = P[i] ^ streamtoword(key, koffp); P[i] = P[i] ^ streamtoword(key, koffp);
for (i = 0; i < plen; i += 2) { for (i = 0; i < plen; i += 2)
{
lr[0] ^= streamtoword(data, doffp); lr[0] ^= streamtoword(data, doffp);
lr[1] ^= streamtoword(data, doffp); lr[1] ^= streamtoword(data, doffp);
encipher(lr, 0); encipher(lr, 0);
@ -586,7 +605,8 @@ public class BCrypt {
P[i + 1] = lr[1]; P[i + 1] = lr[1];
} }
for (i = 0; i < slen; i += 2) { for (i = 0; i < slen; i += 2)
{
lr[0] ^= streamtoword(data, doffp); lr[0] ^= streamtoword(data, doffp);
lr[1] ^= streamtoword(data, doffp); lr[1] ^= streamtoword(data, doffp);
encipher(lr, 0); encipher(lr, 0);
@ -604,7 +624,8 @@ public class BCrypt {
* of rounds of hashing to apply * of rounds of hashing to apply
* @return an array containing the binary hashed password * @return an array containing the binary hashed password
*/ */
private byte[] crypt_raw(byte password[], byte salt[], int log_rounds) { private byte[] crypt_raw(byte password[], byte salt[], int log_rounds)
{
int rounds, i, j; int rounds, i, j;
int cdata[] = (int[])bf_crypt_ciphertext.clone(); int cdata[] = (int[])bf_crypt_ciphertext.clone();
int clen = cdata.length; int clen = cdata.length;
@ -612,24 +633,28 @@ public class BCrypt {
if (log_rounds < 4 || log_rounds > 31) if (log_rounds < 4 || log_rounds > 31)
throw new IllegalArgumentException ("Bad number of rounds"); throw new IllegalArgumentException ("Bad number of rounds");
rounds = 1 << log_rounds; rounds = 1 << log_rounds;
if (salt.length != BCRYPT_SALT_LEN) if (salt.length != BCRYPT_SALT_LEN)
throw new IllegalArgumentException ("Bad salt length"); throw new IllegalArgumentException ("Bad salt length");
init_key(); init_key();
ekskey(salt, password); ekskey(salt, password);
for (i = 0; i < rounds; i++) { for (i = 0; i < rounds; i++)
{
key(password); key(password);
key(salt); key(salt);
} }
for (i = 0; i < 64; i++) { for (i = 0; i < 64; i++)
{
for (j = 0; j < (clen >> 1); j++) for (j = 0; j < (clen >> 1); j++)
encipher(cdata, j << 1); encipher(cdata, j << 1);
} }
ret = new byte[clen * 4]; ret = new byte[clen * 4];
for (i = 0, j = 0; i < clen; i++) { for (i = 0, j = 0; i < clen; i++)
{
ret[j++] = (byte)((cdata[i] >> 24) & 0xff); ret[j++] = (byte)((cdata[i] >> 24) & 0xff);
ret[j++] = (byte)((cdata[i] >> 16) & 0xff); ret[j++] = (byte)((cdata[i] >> 16) & 0xff);
ret[j++] = (byte)((cdata[i] >> 8) & 0xff); ret[j++] = (byte)((cdata[i] >> 8) & 0xff);
@ -645,7 +670,8 @@ public class BCrypt {
* using BCrypt.gensalt) * using BCrypt.gensalt)
* @return the hashed password * @return the hashed password
*/ */
public static String hashpw(String password, String salt) { public static String hashpw(String password, String salt)
{
BCrypt B; BCrypt B;
String real_salt; String real_salt;
byte passwordb[], saltb[], hashed[]; byte passwordb[], saltb[], hashed[];
@ -655,9 +681,11 @@ public class BCrypt {
if (salt.charAt(0) != '$' || salt.charAt(1) != '2') if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
throw new IllegalArgumentException ("Invalid salt version"); throw new IllegalArgumentException ("Invalid salt version");
if (salt.charAt(2) == '$') if (salt.charAt(2) == '$')
off = 3; off = 3;
else { else
{
minor = salt.charAt(2); minor = salt.charAt(2);
if (minor != 'a' || salt.charAt(3) != '$') if (minor != 'a' || salt.charAt(3) != '$')
throw new IllegalArgumentException ("Invalid salt revision"); throw new IllegalArgumentException ("Invalid salt revision");
@ -670,9 +698,12 @@ public class BCrypt {
rounds = Integer.parseInt(salt.substring(off, off + 2)); rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25); real_salt = salt.substring(off + 3, off + 25);
try { try
{
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) { }
catch (UnsupportedEncodingException uee)
{
throw new AssertionError("UTF-8 is not supported"); throw new AssertionError("UTF-8 is not supported");
} }
@ -684,14 +715,17 @@ public class BCrypt {
rs.append("$2"); rs.append("$2");
if (minor >= 'a') if (minor >= 'a')
rs.append(minor); rs.append(minor);
rs.append("$"); rs.append("$");
if (rounds < 10) if (rounds < 10)
rs.append("0"); rs.append("0");
rs.append(Integer.toString(rounds)); rs.append(Integer.toString(rounds));
rs.append("$"); rs.append("$");
rs.append(encode_base64(saltb, saltb.length)); rs.append(encode_base64(saltb, saltb.length));
rs.append(encode_base64(hashed, rs.append(encode_base64(hashed,
bf_crypt_ciphertext.length * 4 - 1)); bf_crypt_ciphertext.length * 4 - 1));
return rs.toString(); return rs.toString();
} }
@ -703,7 +737,8 @@ public class BCrypt {
* @param random an instance of SecureRandom to use * @param random an instance of SecureRandom to use
* @return an encoded salt value * @return an encoded salt value
*/ */
public static String gensalt(int log_rounds, SecureRandom random) { public static String gensalt(int log_rounds, SecureRandom random)
{
StringBuffer rs = new StringBuffer(); StringBuffer rs = new StringBuffer();
byte rnd[] = new byte[BCRYPT_SALT_LEN]; byte rnd[] = new byte[BCRYPT_SALT_LEN];
@ -712,6 +747,7 @@ public class BCrypt {
rs.append("$2a$"); rs.append("$2a$");
if (log_rounds < 10) if (log_rounds < 10)
rs.append("0"); rs.append("0");
rs.append(Integer.toString(log_rounds)); rs.append(Integer.toString(log_rounds));
rs.append("$"); rs.append("$");
rs.append(encode_base64(rnd, rnd.length)); rs.append(encode_base64(rnd, rnd.length));
@ -725,7 +761,8 @@ public class BCrypt {
* 2**log_rounds. * 2**log_rounds.
* @return an encoded salt value * @return an encoded salt value
*/ */
public static String gensalt(int log_rounds) { public static String gensalt(int log_rounds)
{
return gensalt(log_rounds, new SecureRandom()); return gensalt(log_rounds, new SecureRandom());
} }
@ -735,7 +772,8 @@ public class BCrypt {
* rounds to apply * rounds to apply
* @return an encoded salt value * @return an encoded salt value
*/ */
public static String gensalt() { public static String gensalt()
{
return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
} }
@ -746,7 +784,8 @@ public class BCrypt {
* @param hashed the previously-hashed password * @param hashed the previously-hashed password
* @return true if the passwords match, false otherwise * @return true if the passwords match, false otherwise
*/ */
public static boolean checkpw(String plaintext, String hashed) { public static boolean checkpw(String plaintext, String hashed)
{
return (hashed.compareTo(hashpw(plaintext, hashed)) == 0); return (hashed.compareTo(hashpw(plaintext, hashed)) == 0);
} }
} }

View File

@ -11,8 +11,8 @@ 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>(); private Map<String, Integer> FailCounts = new HashMap<String, Integer>();
@Override @Override
@ -20,7 +20,7 @@ public class ConfigPasswordAuthenticator implements PasswordAuthenticator {
{ {
// Depending on our hash type, we have to try and figure out what we're doing. // Depending on our hash type, we have to try and figure out what we're doing.
String HashType = SshdPlugin.instance.getConfig().getString("PasswordType"); String HashType = SshdPlugin.instance.getConfig().getString("PasswordType");
String ConfigHash = SshdPlugin.instance.getConfig().getString("Credentials." + username.trim()); String ConfigHash = SshdPlugin.instance.getConfig().getString("Credentials." + username.trim() + ".password");
if (ConfigHash == null) if (ConfigHash == null)
SshdPlugin.instance.getLogger().warning("Config has no such user: " + username); SshdPlugin.instance.getLogger().warning("Config has no such user: " + username);
@ -70,7 +70,7 @@ public class ConfigPasswordAuthenticator implements PasswordAuthenticator {
} }
SshdPlugin.instance.getLogger().info("Failed login for " + username + " using " + HashType + "-based password authentication."); SshdPlugin.instance.getLogger().info("Failed login for " + username + " using " + HashType + "-based password authentication.");
Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries"); Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries", 3);
try try
{ {

View File

@ -14,51 +14,63 @@ 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(ChannelSession cs, 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;
private OutputStream out; private OutputStream out;
private OutputStream err; private OutputStream err;
private ExitCallback callback; private ExitCallback callback;
public ConsoleCommand(String command) { public ConsoleCommand(String command)
{
this.command = command; this.command = command;
} }
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;
} }
@Override @Override
public void start(ChannelSession cs, Environment environment) throws IOException { public void start(ChannelSession cs, Environment environment) throws IOException
try { {
try
{
SshdPlugin.instance.getLogger() SshdPlugin.instance.getLogger()
.info("[U: " + environment.getEnv().get(Environment.ENV_USER) + "] " + command); .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 -" + e.getMessage()); SshdPlugin.instance.getLogger().severe("Error processing command from SSH -" + e.getMessage());
} finally { }
finally
{
callback.onExit(0); callback.onExit(0);
} }
} }

View File

@ -96,7 +96,7 @@ public class ConsoleLogFormatter extends Formatter {
stringbuilder.append(stringwriter.toString()); stringbuilder.append(stringwriter.toString());
} }
return stringbuilder.toString(); return stringbuilder.toString().replace("\n", "\r\n");
} }
private void colorize(LogRecord logrecord) private void colorize(LogRecord logrecord)

View File

@ -140,10 +140,10 @@ class Cryptography
private static byte[] FromHex(String hex) throws NoSuchAlgorithmException private static byte[] FromHex(String hex) throws NoSuchAlgorithmException
{ {
byte[] bytes = new byte[hex.length() / 2]; byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < bytes.length; i++) for (int i = 0; i < bytes.length; i++)
{
bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16); bytes[i] = (byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return bytes; return bytes;
} }
@ -152,6 +152,7 @@ class Cryptography
BigInteger bi = new BigInteger(1, array); BigInteger bi = new BigInteger(1, array);
String hex = bi.toString(16); String hex = bi.toString(16);
int paddingLength = (array.length * 2) - hex.length(); int paddingLength = (array.length * 2) - hex.length();
if (paddingLength > 0) if (paddingLength > 0)
return String.format("%0" + paddingLength + "d", 0) + hex; return String.format("%0" + paddingLength + "d", 0) + hex;
else else

View File

@ -10,39 +10,51 @@ 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)
{
super(out, formatter); super(out, formatter);
this.reader = reader; this.reader = reader;
setLevel(Level.INFO); setLevel(Level.INFO);
} }
@Override @Override
public synchronized void publish(LogRecord record) { public synchronized void publish(LogRecord record)
{
record.setMessage(record.getMessage().replace("\n", "\n\r")); record.setMessage(record.getMessage().replace("\n", "\n\r"));
super.publish(record); super.publish(record);
flush(); flush();
} }
@Override @Override
public synchronized void flush() { public synchronized void flush()
try { {
try
{
reader.print(ConsoleReader.RESET_LINE + ""); reader.print(ConsoleReader.RESET_LINE + "");
reader.flush(); reader.flush();
super.flush(); super.flush();
try { try
{
reader.drawLine(); reader.drawLine();
} catch (Throwable ex) { }
catch (Throwable ex)
{
reader.getCursorBuffer().clear(); reader.getCursorBuffer().clear();
} }
reader.flush(); reader.flush();
super.flush(); super.flush();
} catch (SshException ex) { }
catch (SshException ex)
{
// do nothing // do nothing
} catch (IOException ex) { }
catch (IOException ex)
{
Logger.getLogger(FlushyStreamHandler.class.getName()).log(Level.SEVERE, null, ex); Logger.getLogger(FlushyStreamHandler.class.getName()).log(Level.SEVERE, null, ex);
} }
} }

View File

@ -0,0 +1,99 @@
package com.ryanmichela.sshd;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import java.util.Arrays;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import com.ryanmichela.sshd.Cryptography;
import com.ryanmichela.sshd.SshdPlugin;
class MkpasswdCommand implements CommandExecutor
{
// Because Spigot's failed syntax API is really less than ideal (you should be required to add a
// SendSyntax function override), we're just always going to return true even for syntax failures
// as we will handle the syntax message internally. This also lets us send the messages more
// securely to the client without people knowing we're using the command. This prevents password
// or hash leakages from the user to other connected users. Plus this syntax will show how
// to both use the command and what hashes we support which is important for people who don't
// know how to RTFM. - Justin
private void SendSyntax(CommandSender sender, boolean invalid)
{
if (invalid)
sender.sendMessage("\u00A7cInvalid Syntax\u00A7r");
sender.sendMessage("\u00A7a/mkpasswd <help|hash> <password>\u00A7r");
sender.sendMessage("\u00A79Supported Hashes: SHA256, PBKDF2, BCRYPT, PLAIN\u00A7r");
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args)
{
String algoritm, password;
try
{
// Stupid bukkit, we have to concatenate the arguments together if they're using
// spaces in their passwords otherwise it won't be as strong as it should be.
algoritm = args[0];
password = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (password.trim().isEmpty()) // Shortcut to the catch statement below.
throw new ArrayIndexOutOfBoundsException();
}
catch (ArrayIndexOutOfBoundsException e)
{
// ignore it.
this.SendSyntax(sender, true);
return true;
}
boolean hasperm = (sender instanceof Player) ? ((Player)sender).hasPermission("sshd.mkpasswd") : true;
if (hasperm)
{
try
{
String hash = "";
// Dumb but whatever. Some people are really dense.
if (algoritm.equalsIgnoreCase("PLAIN"))
{
// I mean c'mon...
sender.sendMessage("\u00A79Your Hash: \u00A7cIt's literally your unhashed password.");
return true;
}
else if (algoritm.equalsIgnoreCase("pbkdf2"))
hash = Cryptography.PBKDF2_HashPassword(password);
else if (algoritm.equalsIgnoreCase("bcrypt"))
hash = Cryptography.BCrypt_HashPassword(password);
else if (algoritm.equalsIgnoreCase("sha256"))
hash = Cryptography.SHA256_HashPassword(password);
else
{
this.SendSyntax(sender, !algoritm.equalsIgnoreCase("help"));
return true;
}
TextComponent msg = new TextComponent("\u00A79Your Hash: " + hash + "\u00A7r");
msg.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash));
msg.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("Click to copy the hash!").create()));
sender.spigot().sendMessage(msg);
}
catch (Exception e)
{
// We're console, just print the stack trace.
e.printStackTrace();
sender.sendMessage("\u00A7cAn error occured. Please check console for details.\u00A7r");
}
}
else
sender.sendMessage("\u00A7cPermission Denied.\u00A7r");
return true;
}
}

View File

@ -0,0 +1,23 @@
package com.ryanmichela.sshd;
import java.util.Optional;
import com.ryanmichela.sshd.SshdPlugin;
public class PermissionUtil
{
public static Optional<String> GetCredential(String username, String credential)
{
String Default = SshdPlugin.instance.getConfig().getString("Credentials.$default." + credential);
String cred = SshdPlugin.instance.getConfig().getString("Credentials." + username + "." + credential, Default);
if (cred == null)
return Optional.empty();
else if (cred.isEmpty())
return Optional.empty();
else
return Optional.of(cred);
}
};

View File

@ -1,6 +1,5 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd;
import org.apache.commons.lang.ArrayUtils;
import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
@ -27,7 +26,7 @@ public class PublicKeyAuthenticator implements PublickeyAuthenticator
{ {
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"); Integer tries = SshdPlugin.instance.getConfig().getInt("LoginRetries", 3);
if (keyFile.exists()) if (keyFile.exists())
{ {
@ -68,7 +67,8 @@ public class PublicKeyAuthenticator implements PublickeyAuthenticator
} }
catch (Exception e) catch (Exception e)
{ {
SshdPlugin.instance.getLogger().severe("Failed to process public key " + keyFile.getAbsolutePath() + " " + e.getMessage()); e.printStackTrace();
SshdPlugin.instance.getLogger().severe("Failed to process public key " + keyFile.getAbsolutePath());
} }
} }
else else

View File

@ -9,16 +9,20 @@ import java.lang.reflect.Modifier;
*/ */
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);
} }
public static void setProtectedValue(Class c, String field, Object newValue) { public static void setProtectedValue(Class c, String field, Object newValue)
{
setProtectedValue(c, null, field, newValue); setProtectedValue(c, null, field, newValue);
} }
public static void setProtectedValue(Class c, Object o, String field, Object newValue) { public static void setProtectedValue(Class c, Object o, String field, Object newValue)
try { {
try
{
Field f = c.getDeclaredField(field); Field f = c.getDeclaredField(field);
@ -29,18 +33,25 @@ 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 | IllegalAccessException ex) { }
catch (NoSuchFieldException | IllegalAccessException ex)
{
System.out.println("*** " + c.getName() + ":" + ex); System.out.println("*** " + c.getName() + ":" + ex);
} }
} }
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);
} }
@ -49,46 +60,58 @@ public class ReflectionUtil {
} }
System.out.println("*** " + obj.getClass().getName() + ":No such field"); System.out.println("*** " + obj.getClass().getName() + ":No such field");
return null; return null;
} catch (Exception ex) { }
catch (Exception ex)
{
System.out.println("*** " + obj.getClass().getName() + ":" + ex); System.out.println("*** " + obj.getClass().getName() + ":" + ex);
return null; return null;
} }
} }
public static <T> T getProtectedValue(Class c, String field) { public static <T> T getProtectedValue(Class c, String field)
try { {
try
{
Field f = c.getDeclaredField(field); Field f = c.getDeclaredField(field);
f.setAccessible(true); f.setAccessible(true);
return (T) f.get(c); return (T) f.get(c);
} catch (Exception ex) { }
catch (Exception ex)
{
System.out.println("*** " + c.getName() + ":" + ex); System.out.println("*** " + c.getName() + ":" + ex);
return null; return null;
} }
} }
public static Object invokeProtectedMethod(Class c, String method, Object... args) { public static Object invokeProtectedMethod(Class c, String method, Object... args)
{
return invokeProtectedMethod(c, null, method, args); return invokeProtectedMethod(c, null, method, args);
} }
public static Object invokeProtectedMethod(Object o, String method, Object... args) { public static Object invokeProtectedMethod(Object o, String method, Object... args)
{
return invokeProtectedMethod(o.getClass(), o, method, args); return invokeProtectedMethod(o.getClass(), o, method, args);
} }
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
pTypes[i] = args[i].getClass(); pTypes[i] = args[i].getClass();
}
} }
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

@ -0,0 +1,210 @@
package com.ryanmichela.sshd;
import com.ryanmichela.sshd.jline.ConsoleShellFactory;
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.contrib.server.subsystem.sftp.SimpleAccessControlSftpEventListener;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Level;
/**
* Copyright 2013 Ryan Michela
*/
public class SshdPlugin extends JavaPlugin
{
private SshServer sshd;
public static SshdPlugin instance;
public static List<ConfigurationSection> GetSections(ConfigurationSection source)
{
if (source == null)
return null;
List<ConfigurationSection> nodes = new ArrayList<ConfigurationSection>();
for (String key : source.getKeys(false))
{
if (source.isConfigurationSection(key))
nodes.add(source.getConfigurationSection(key));
}
return nodes;
}
@Override public void onLoad()
{
saveDefaultConfig();
File authorizedKeys = new File(getDataFolder(), "authorized_keys");
if (!authorizedKeys.exists())
authorizedKeys.mkdirs();
try
{
File motd = new File(getDataFolder(), "motd.txt");
if (!motd.exists())
{
InputStream link = (getClass().getResourceAsStream("/motd.txt"));
Files.copy(link, motd.getAbsoluteFile().toPath());
}
}
catch (IOException e)
{
e.printStackTrace();
}
// Don't go any lower than INFO or SSHD will cause a stack overflow exception.
// SSHD will log that it wrote bites to the output stream, which writes
// bytes to the output stream - ad nauseaum.
getLogger().setLevel(Level.INFO);
}
@Override public void onEnable()
{
instance = this;
sshd = SshServer.setUpDefaultServer();
sshd.setPort(getConfig().getInt("Port", 1025));
String host = getConfig().getString("ListenAddress", "all");
sshd.setHost(host.equals("all") ? null : host);
File hostKey = new File(getDataFolder(), "hostkey");
File authorizedKeys = new File(getDataFolder(), "authorized_keys");
sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKey.toPath()));
sshd.setShellFactory(new ConsoleShellFactory());
sshd.setPasswordAuthenticator(new ConfigPasswordAuthenticator());
sshd.setPublickeyAuthenticator(new PublicKeyAuthenticator(authorizedKeys));
if (getConfig().getBoolean("EnableSFTP", false))
{
// Handle access control for SFTP.
SftpSubsystemFactory.Builder builder = new SftpSubsystemFactory.Builder();
builder.addSftpEventListener(new SimpleAccessControlSftpEventListener()
{
protected boolean isAccessAllowed(ServerSession session, String remote, Path localpath)
{
try
{
ConfigurationSection UsernameNamespace = getConfig().getConfigurationSection("Credentials." + session.getUsername() + ".sftp");
// They don't have SFTP enabled so deny them.
if (UsernameNamespace == null || !UsernameNamespace.getBoolean("enabled"))
return false;
List<ConfigurationSection> rules = GetSections(UsernameNamespace.getConfigurationSection("rules"));
if (rules != null)
{
for (ConfigurationSection path : rules)
{
// Check if the requesting path matches
if (localpath.toString().matches(path.getName()))
{
// Check if they have read permissions
if (path.getBoolean("readable"))
return true;
getLogger().info(String.format("Denied %s read access to \"%s\" matching rule \"%s\"", session.getUsername(), localpath.toString(), path.getName()));
return false;
}
}
}
return UsernameNamespace.getString("default").equalsIgnoreCase("allow");
}
catch (Exception e)
{
e.printStackTrace();
// Automatically deny.
return false;
}
}
protected boolean isModificationAllowed(ServerSession session, String remote, Path localpath)
{
try
{
boolean defaultbool = getConfig().getBoolean("Credentials.$default.sftp.enabled", false);
ConfigurationSection UsernameNamespace = getConfig().getConfigurationSection("Credentials." + session.getUsername() + ".sftp");
// They don't have SFTP enabled so deny them.
if (UsernameNamespace == null || !UsernameNamespace.getBoolean("enabled", defaultbool))
return false;
// Check a list of files against a path trying to be accessed.
List<ConfigurationSection> rules = GetSections(UsernameNamespace.getConfigurationSection("rules"));
if (rules != null)
{
for (ConfigurationSection path : rules)
{
// Check if the requesting path matches
if (localpath.toString().matches(path.getName()))
{
// Check if they have read permissions
if (path.getBoolean("writeable"))
return true;
getLogger().info(String.format("Denied %s modifications to \"%s\" matching rule \"%s\"", session.getUsername(), localpath.toString(), path.getName()));
return false;
}
}
}
return UsernameNamespace.getString("default", "deny").equalsIgnoreCase("allow");
}
catch (Exception e)
{
e.printStackTrace();
// Automatically deny.
return false;
}
}
});
sshd.setSubsystemFactories(Collections.singletonList(builder.build()));
sshd.setFileSystemFactory(new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent()));
}
this.getCommand("mkpasswd").setExecutor(new MkpasswdCommand());
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
{
// Terminate any active sessions
for (AbstractSession as : sshd.getActiveSessions())
as.close(true);
// Pass "true" to stop immediately!
sshd.stop(true);
}
catch (Exception e)
{
// do nothing
e.printStackTrace();
}
}
}

View File

@ -13,80 +13,99 @@ import java.util.logging.StreamHandler;
/** /**
* Copyright 2014 Ryan Michela * Copyright 2014 Ryan Michela
*/ */
public class StreamHandlerAppender implements Appender { public class StreamHandlerAppender implements Appender
{
private StreamHandler streamHandler; private StreamHandler streamHandler;
private UUID uuid; private UUID uuid;
public StreamHandlerAppender(StreamHandler streamHandler) { public StreamHandlerAppender(StreamHandler streamHandler)
{
this.streamHandler = streamHandler; this.streamHandler = streamHandler;
uuid = UUID.randomUUID(); uuid = UUID.randomUUID();
} }
@Override @Override
public void append(LogEvent logEvent) { public void append(LogEvent logEvent)
{
java.util.logging.Level level; java.util.logging.Level level;
if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.DEBUG)) { if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.DEBUG))
level = java.util.logging.Level.FINE; level = java.util.logging.Level.FINE;
} else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.INFO)) { else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.INFO))
level = java.util.logging.Level.INFO; level = java.util.logging.Level.INFO;
} else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.WARN)) { else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.WARN))
level = java.util.logging.Level.WARNING; level = java.util.logging.Level.WARNING;
} else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.ERROR)) { else if (logEvent.getLevel().equals(org.apache.logging.log4j.Level.ERROR))
level = java.util.logging.Level.SEVERE; level = java.util.logging.Level.SEVERE;
} else { else
level = java.util.logging.Level.INFO; level = java.util.logging.Level.INFO;
}
String message = logEvent.getMessage().getFormattedMessage(); String message = logEvent.getMessage().getFormattedMessage();
LogRecord logRecord = new LogRecord(level, message); LogRecord logRecord = new LogRecord(level, message);
streamHandler.publish(logRecord); streamHandler.publish(logRecord);
} }
@Override @Override
public String getName() { public String getName()
{
return "StreamHandlerAppender:" + uuid.toString(); return "StreamHandlerAppender:" + uuid.toString();
} }
@Override @Override
public Layout<? extends Serializable> getLayout() { public Layout<? extends Serializable> getLayout()
{
return null; return null;
} }
@Override @Override
public boolean ignoreExceptions() { public boolean ignoreExceptions()
{
return false; return false;
} }
@Override @Override
public ErrorHandler getHandler() { public ErrorHandler getHandler()
{
return null; return null;
} }
@Override @Override
public void setHandler(ErrorHandler errorHandler) { public void setHandler(ErrorHandler errorHandler)
{
} }
@Override @Override
public void start() { public State getState() {
// Todo: i am not sure what is this
return null;
} }
@Override @Override
public void stop() { public void initialize() {
// Todo: i am not sure what is this
} }
@Override @Override
public boolean isStarted() { public void start()
{
}
@Override
public void stop()
{
}
@Override
public boolean isStarted()
{
return true; return true;
} }
@Override @Override
public boolean isStopped() { public boolean isStopped()
{
return false; return false;
} }
} }

View File

@ -5,9 +5,11 @@ 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,
@ -17,19 +19,28 @@ public abstract class Waitable<T> implements Runnable {
T value = null; T value = null;
Status status = Status.WAITING; Status status = Status.WAITING;
public final void run() { public final void run()
synchronized (this) { {
if (status != Status.WAITING) { synchronized (this)
{
if (status != Status.WAITING)
throw new IllegalStateException("Invalid state " + status); throw new IllegalStateException("Invalid state " + status);
}
status = Status.RUNNING; status = Status.RUNNING;
} }
try {
try
{
value = evaluate(); value = evaluate();
} catch (Throwable t) { }
catch (Throwable t)
{
this.t = t; this.t = t;
} finally { }
synchronized (this) { finally
{
synchronized (this)
{
status = Status.FINISHED; status = Status.FINISHED;
this.notifyAll(); this.notifyAll();
} }
@ -38,13 +49,14 @@ public abstract class Waitable<T> implements Runnable {
protected abstract T evaluate(); protected abstract T evaluate();
public synchronized T get() throws InterruptedException, ExecutionException { public synchronized T get() throws InterruptedException, ExecutionException
while (status != Status.FINISHED) { {
while (status != Status.FINISHED)
this.wait(); this.wait();
}
if (t != null) { if (t != null)
throw new ExecutionException(t); throw new ExecutionException(t);
}
return value; return value;
} }
} }

View File

@ -1,8 +1,7 @@
package com.ryanmichela.sshd.implementations; package com.ryanmichela.sshd.implementations.commandsenders;
import com.ryanmichela.sshd.SshdPlugin; import com.ryanmichela.sshd.SshdPlugin;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.ConsoleCommandSender;
@ -15,22 +14,25 @@ import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import com.ryanmichela.sshd.ConsoleShellFactory; import com.ryanmichela.sshd.jline.ConsoleShellFactory;
import com.ryanmichela.sshd.ConsoleLogFormatter;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
public class SSHDCommandSender implements ConsoleCommandSender, CommandSender { public class SSHDCommandSender implements ConsoleCommandSender, CommandSender
{
private final PermissibleBase perm = new PermissibleBase(this); private final PermissibleBase perm = new PermissibleBase(this);
private final SSHDConversationTracker conversationTracker = new SSHDConversationTracker(); private final SSHDConversationTracker conversationTracker = new SSHDConversationTracker();
// Set by the upstream allocating function // Set by the upstream allocating function
public ConsoleShellFactory.ConsoleShell console; public ConsoleShellFactory.ConsoleShell console;
public void sendMessage(String message) { public void sendMessage(String message)
this.sendRawMessage(message); {
this.sendRawMessage(message + "\r");
} }
public void sendRawMessage(String message) public void sendRawMessage(String message)
@ -40,7 +42,18 @@ public class SSHDCommandSender implements ConsoleCommandSender, CommandSender {
return; return;
try try
{ {
this.console.ConsoleReader.println(ChatColor.stripColor(message)); this.console.ConsoleReader.println(ConsoleLogFormatter.ColorizeString(message).replace("\n", "\n\r"));
this.console.ConsoleReader.print(this.console.ConsoleReader.RESET_LINE + "");
this.console.ConsoleReader.flush();
try
{
this.console.ConsoleReader.drawLine();
}
catch (Throwable ex)
{
this.console.ConsoleReader.getCursorBuffer().clear();
}
this.console.ConsoleReader.flush();
} }
catch (IOException e) catch (IOException e)
{ {
@ -48,92 +61,133 @@ public class SSHDCommandSender implements ConsoleCommandSender, CommandSender {
} }
} }
public void sendMessage(String[] messages) { @Override
public void sendRawMessage(UUID uuid, String s) {
sendRawMessage(s);
}
public void sendMessage(String[] messages)
{
Arrays.asList(messages).forEach(this::sendMessage); Arrays.asList(messages).forEach(this::sendMessage);
} }
public String getName() { @Override
public void sendMessage(UUID uuid, String s) {
sendMessage(s);
}
@Override
public void sendMessage(UUID uuid, String... strings) {
sendMessage(strings);
}
public String getName()
{
return "SSHD Console"; return "SSHD Console";
} }
public boolean isOp() { public boolean isOp()
{
return true; return true;
} }
public void setOp(boolean value) { public void setOp(boolean value)
{
throw new UnsupportedOperationException("Cannot change operator status of server console"); throw new UnsupportedOperationException("Cannot change operator status of server console");
} }
public boolean beginConversation(Conversation conversation) { public boolean beginConversation(Conversation conversation)
{
return this.conversationTracker.beginConversation(conversation); return this.conversationTracker.beginConversation(conversation);
} }
public void abandonConversation(Conversation conversation) { public void abandonConversation(Conversation conversation)
{
this.conversationTracker.abandonConversation(conversation, new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller())); this.conversationTracker.abandonConversation(conversation, new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller()));
} }
public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details)
{
this.conversationTracker.abandonConversation(conversation, details); this.conversationTracker.abandonConversation(conversation, details);
} }
public void acceptConversationInput(String input) { public void acceptConversationInput(String input)
{
this.conversationTracker.acceptConversationInput(input); this.conversationTracker.acceptConversationInput(input);
} }
public boolean isConversing() { public boolean isConversing()
{
return this.conversationTracker.isConversing(); return this.conversationTracker.isConversing();
} }
public boolean isPermissionSet(String name) { public boolean isPermissionSet(String name)
{
return this.perm.isPermissionSet(name); return this.perm.isPermissionSet(name);
} }
public boolean isPermissionSet(Permission perm) { public boolean isPermissionSet(Permission perm)
{
return this.perm.isPermissionSet(perm); return this.perm.isPermissionSet(perm);
} }
public boolean hasPermission(String name) { public boolean hasPermission(String name)
{
return this.perm.hasPermission(name); return this.perm.hasPermission(name);
} }
public boolean hasPermission(Permission perm) { public boolean hasPermission(Permission perm)
{
return this.perm.hasPermission(perm); return this.perm.hasPermission(perm);
} }
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) { public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value)
{
return this.perm.addAttachment(plugin, name, value); return this.perm.addAttachment(plugin, name, value);
} }
public PermissionAttachment addAttachment(Plugin plugin) { public PermissionAttachment addAttachment(Plugin plugin)
{
return this.perm.addAttachment(plugin); return this.perm.addAttachment(plugin);
} }
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) { public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks)
{
return this.perm.addAttachment(plugin, name, value, ticks); return this.perm.addAttachment(plugin, name, value, ticks);
} }
public PermissionAttachment addAttachment(Plugin plugin, int ticks) { public PermissionAttachment addAttachment(Plugin plugin, int ticks)
{
return this.perm.addAttachment(plugin, ticks); return this.perm.addAttachment(plugin, ticks);
} }
public void removeAttachment(PermissionAttachment attachment) { public void removeAttachment(PermissionAttachment attachment)
{
this.perm.removeAttachment(attachment); this.perm.removeAttachment(attachment);
} }
public void recalculatePermissions() { public void recalculatePermissions()
{
this.perm.recalculatePermissions(); this.perm.recalculatePermissions();
} }
public Set<PermissionAttachmentInfo> getEffectivePermissions() { public Set<PermissionAttachmentInfo> getEffectivePermissions()
{
return this.perm.getEffectivePermissions(); return this.perm.getEffectivePermissions();
} }
public boolean isPlayer() { public boolean isPlayer()
{
return false; return false;
} }
public Server getServer() { public Server getServer()
{
return Bukkit.getServer(); return Bukkit.getServer();
} }
public CommandSender.Spigot spigot()
{
return ((CommandSender)this).spigot();
}
} }

View File

@ -1,4 +1,4 @@
package com.ryanmichela.sshd.implementations; package com.ryanmichela.sshd.implementations.commandsenders;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.conversations.Conversation; import org.bukkit.conversations.Conversation;
@ -8,13 +8,17 @@ import org.bukkit.conversations.ManuallyAbandonedConversationCanceller;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.logging.Level; import java.util.logging.Level;
public class SSHDConversationTracker { public class SSHDConversationTracker
{
private LinkedList<Conversation> conversationQueue = new LinkedList<>(); private LinkedList<Conversation> conversationQueue = new LinkedList<>();
synchronized boolean beginConversation(Conversation conversation) { synchronized boolean beginConversation(Conversation conversation)
if (!this.conversationQueue.contains(conversation)) { {
if (!this.conversationQueue.contains(conversation))
{
this.conversationQueue.addLast(conversation); this.conversationQueue.addLast(conversation);
if (this.conversationQueue.getFirst() == conversation) { if (this.conversationQueue.getFirst() == conversation)
{
conversation.begin(); conversation.begin();
conversation.outputNextPrompt(); conversation.outputNextPrompt();
return true; return true;
@ -24,55 +28,66 @@ public class SSHDConversationTracker {
return true; return true;
} }
synchronized void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) { synchronized void abandonConversation(Conversation conversation, ConversationAbandonedEvent details)
if (!this.conversationQueue.isEmpty()) { {
if (this.conversationQueue.getFirst() == conversation) { if (!this.conversationQueue.isEmpty())
{
if (this.conversationQueue.getFirst() == conversation)
conversation.abandon(details); conversation.abandon(details);
}
if (this.conversationQueue.contains(conversation)) { if (this.conversationQueue.contains(conversation))
this.conversationQueue.remove(conversation); this.conversationQueue.remove(conversation);
}
if (!this.conversationQueue.isEmpty()) { if (!this.conversationQueue.isEmpty())
this.conversationQueue.getFirst().outputNextPrompt(); this.conversationQueue.getFirst().outputNextPrompt();
}
} }
} }
public synchronized void abandonAllConversations() { public synchronized void abandonAllConversations()
{
LinkedList<Conversation> oldQueue = this.conversationQueue; LinkedList<Conversation> oldQueue = this.conversationQueue;
this.conversationQueue = new LinkedList<>(); this.conversationQueue = new LinkedList<>();
for (Conversation conversation : oldQueue) { for (Conversation conversation : oldQueue)
try { {
try
{
conversation.abandon(new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller())); conversation.abandon(new ConversationAbandonedEvent(conversation, new ManuallyAbandonedConversationCanceller()));
} catch (Throwable var5) { }
catch (Throwable var5)
{
Bukkit.getLogger().log(Level.SEVERE, "Unexpected exception while abandoning a conversation", var5); Bukkit.getLogger().log(Level.SEVERE, "Unexpected exception while abandoning a conversation", var5);
} }
} }
} }
synchronized void acceptConversationInput(String input) { synchronized void acceptConversationInput(String input)
if (this.isConversing()) { {
if (this.isConversing())
{
Conversation conversation = this.conversationQueue.getFirst(); Conversation conversation = this.conversationQueue.getFirst();
try { try
{
conversation.acceptInput(input); conversation.acceptInput(input);
} catch (Throwable var4) { }
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); 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() { synchronized boolean isConversing()
{
return !this.conversationQueue.isEmpty(); return !this.conversationQueue.isEmpty();
} }
public synchronized boolean isConversingModaly() { public synchronized boolean isConversingModaly()
{
return this.isConversing() && this.conversationQueue.getFirst().isModal(); return this.isConversing() && this.conversationQueue.getFirst().isModal();
} }
} }

View File

@ -1,9 +1,12 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd.jline;
/** /**
* Copyright 2013 Ryan Michela * Copyright 2013 Ryan Michela
*/ */
import com.ryanmichela.sshd.ReflectionUtil;
import com.ryanmichela.sshd.SshdPlugin;
import com.ryanmichela.sshd.Waitable;
import jline.console.completer.Completer; import jline.console.completer.Completer;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandMap; import org.bukkit.command.CommandMap;
@ -12,33 +15,41 @@ 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
protected List<String> evaluate() { protected List<String> evaluate()
{
CommandMap commandMap = ReflectionUtil.getProtectedValue(Bukkit.getServer(), "commandMap"); CommandMap commandMap = ReflectionUtil.getProtectedValue(Bukkit.getServer(), "commandMap");
return commandMap.tabComplete(Bukkit.getServer().getConsoleSender(), buffer); return commandMap.tabComplete(Bukkit.getServer().getConsoleSender(), buffer);
} }
}; };
Bukkit.getScheduler().runTask(SshdPlugin.instance, waitable); Bukkit.getScheduler().runTask(SshdPlugin.instance, waitable);
try { try
{
List<String> offers = waitable.get(); List<String> offers = waitable.get();
if (offers == null) { if (offers == null)
return cursor; return cursor;
}
candidates.addAll(offers); candidates.addAll(offers);
final int lastSpace = buffer.lastIndexOf(' '); final int lastSpace = buffer.lastIndexOf(' ');
if (lastSpace == -1) { if (lastSpace == -1)
return cursor - buffer.length(); return cursor - buffer.length();
} else { else
return cursor - (buffer.length() - lastSpace - 1); return cursor - (buffer.length() - lastSpace - 1);
} }
} catch (ExecutionException e) { catch (ExecutionException e)
{
SshdPlugin.instance.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); SshdPlugin.instance.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e);
} catch (InterruptedException e) { }
catch (InterruptedException e)
{
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
return cursor; return cursor;

View File

@ -1,18 +1,10 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd.jline;
import com.ryanmichela.sshd.ConsoleCommandCompleter; import com.ryanmichela.sshd.*;
import com.ryanmichela.sshd.ConsoleLogFormatter; import com.ryanmichela.sshd.implementations.commandsenders.SSHDCommandSender;
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 jline.console.ConsoleReader;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.core.Logger;
import org.apache.sshd.common.Factory;
import org.apache.sshd.server.shell.ShellFactory; import org.apache.sshd.server.shell.ShellFactory;
import org.apache.sshd.server.command.Command; import org.apache.sshd.server.command.Command;
import org.apache.sshd.server.channel.ChannelSession; import org.apache.sshd.server.channel.ChannelSession;
@ -28,17 +20,21 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.StringTokenizer; import java.net.UnknownHostException;
import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.StreamHandler; import java.util.logging.StreamHandler;
public class ConsoleShellFactory implements ShellFactory { public class ConsoleShellFactory implements ShellFactory
{
public Command createShell(ChannelSession cs) { public Command createShell(ChannelSession cs)
{
return new ConsoleShell(); return new ConsoleShell();
} }
public class ConsoleShell implements Command, Runnable { public class ConsoleShell implements Command, Runnable
{
private InputStream in; private InputStream in;
private OutputStream out; private OutputStream out;
@ -52,35 +48,43 @@ public class ConsoleShellFactory implements ShellFactory {
public ConsoleReader ConsoleReader; public ConsoleReader ConsoleReader;
public SSHDCommandSender SshdCommandSender; 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;
} }
@ -89,6 +93,17 @@ public class ConsoleShellFactory implements ShellFactory {
{ {
try try
{ {
String username = env.getEnv().get(Environment.ENV_USER);
Optional<String> optcred = PermissionUtil.GetCredential(username, "console");
// They don't have access.
if (optcred.isPresent() && !optcred.get().contains("R"))
{
cs.close(true);
return;
}
else
SshdPlugin.instance.getLogger().warning("There is no $default pseudo-user under credential, allowing unrestricted access...");
this.ConsoleReader = new ConsoleReader(in, new FlushyOutputStream(out), new SshTerminal()); this.ConsoleReader = new ConsoleReader(in, new FlushyOutputStream(out), new SshTerminal());
this.ConsoleReader.setExpandEvents(true); this.ConsoleReader.setExpandEvents(true);
this.ConsoleReader.addCompleter(new ConsoleCommandCompleter()); this.ConsoleReader.addCompleter(new ConsoleCommandCompleter());
@ -99,14 +114,15 @@ public class ConsoleShellFactory implements ShellFactory {
((Logger)LogManager.getRootLogger()).addAppender(this.streamHandlerAppender); ((Logger)LogManager.getRootLogger()).addAppender(this.streamHandlerAppender);
this.environment = env; this.environment = env;
this.Username = env.getEnv().get(Environment.ENV_USER); this.Username = username;
this.SshdCommandSender = new SSHDCommandSender(); this.SshdCommandSender = new SSHDCommandSender();
this.SshdCommandSender.console = this; this.SshdCommandSender.console = this;
thread = new Thread(this, "SSHD ConsoleShell " + this.Username); thread = new Thread(this, "SSHD ConsoleShell " + username);
thread.start(); thread.start();
} }
catch (Exception e) catch (Exception e)
{ {
e.printStackTrace();
throw new IOException("Error starting shell", e); throw new IOException("Error starting shell", e);
} }
} }
@ -118,7 +134,7 @@ public class ConsoleShellFactory implements ShellFactory {
{ {
try try
{ {
if (!SshdPlugin.instance.getConfig().getString("Mode").equals("RPC")) if (!SshdPlugin.instance.getConfig().getString("Mode", "DEFAULT").equals("RPC"))
printPreamble(this.ConsoleReader); printPreamble(this.ConsoleReader);
while (true) while (true)
{ {
@ -136,15 +152,21 @@ public class ConsoleShellFactory implements ShellFactory {
if (command.equals("cls")) if (command.equals("cls"))
{ {
this.ConsoleReader.clearScreen(); this.ConsoleReader.clearScreen();
this.ConsoleReader.drawLine();
this.ConsoleReader.flush();
continue; continue;
} }
// Hide the mkpasswd command input from other users. // Hide the mkpasswd command input from other users.
Boolean mkpasswd = command.split(" ")[0].equals("mkpasswd"); Boolean mkpasswd = command.split(" ")[0].equals("mkpasswd");
Optional<String> optcred = PermissionUtil.GetCredential(this.Username, "console");
if (optcred.isPresent() && !optcred.get().contains("W"))
continue;
Bukkit.getScheduler().runTask( Bukkit.getScheduler().runTask(
SshdPlugin.instance, () -> SshdPlugin.instance, () ->
{ {
if (SshdPlugin.instance.getConfig().getString("Mode").equals("RPC") && command.startsWith("rpc")) if (SshdPlugin.instance.getConfig().getString("Mode", "DEFAULT").equals("RPC") && command.startsWith("rpc"))
{ {
// NO ECHO NO PREAMBLE AND SHIT // NO ECHO NO PREAMBLE AND SHIT
String cmd = command.substring("rpc".length() + 1, command.length()); String cmd = command.substring("rpc".length() + 1, command.length());
@ -152,16 +174,25 @@ public class ConsoleShellFactory implements ShellFactory {
} }
else else
{ {
// Don't send our mkpasswd command output. This will echo passwords back
// to the console for all to see. This command is strictly between
// our plugin and the connected client.
if (!mkpasswd) if (!mkpasswd)
{
SshdPlugin.instance.getLogger().info("<" + this.Username + "> " + command); SshdPlugin.instance.getLogger().info("<" + this.Username + "> " + command);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command);
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); }
else
{
Bukkit.dispatchCommand(this.SshdCommandSender, command);
}
} }
}); });
} }
} }
catch (IOException e) catch (IOException e)
{ {
e.printStackTrace();
SshdPlugin.instance.getLogger().log(Level.SEVERE, "Error processing command from SSH", e); SshdPlugin.instance.getLogger().log(Level.SEVERE, "Error processing command from SSH", e);
} }
finally finally
@ -171,6 +202,20 @@ public class ConsoleShellFactory implements ShellFactory {
} }
} }
private String GetHostname()
{
try
{
return InetAddress.getLocalHost().getHostName();
}
catch (UnknownHostException e)
{
e.printStackTrace();
SshdPlugin.instance.getLogger().log(Level.INFO, "The above stacktrace can be ignored, you likely have a misconfigured system hosts file.");
return "Unknown";
}
}
private void printPreamble(ConsoleReader cr) throws IOException private void printPreamble(ConsoleReader cr) throws IOException
{ {
File f = new File(SshdPlugin.instance.getDataFolder(), "motd.txt"); File f = new File(SshdPlugin.instance.getDataFolder(), "motd.txt");
@ -184,13 +229,14 @@ public class ConsoleShellFactory implements ShellFactory {
} }
catch (FileNotFoundException e) catch (FileNotFoundException e)
{ {
e.printStackTrace();
SshdPlugin.instance.getLogger().log(Level.WARNING, "Could not open " + f + ": File does not exist."); SshdPlugin.instance.getLogger().log(Level.WARNING, "Could not open " + f + ": File does not exist.");
// Not showing the SSH motd is not a fatal failure, let the session continue. // Not showing the SSH motd is not a fatal failure, let the session continue.
} }
// Doesn't really guarantee our actual system hostname but // Doesn't really guarantee our actual system hostname but
// it's better than not having one at all. // it's better than not having one at all.
cr.println("Connected to: " + InetAddress.getLocalHost().getHostName() + " (" + Bukkit.getServer().getName() + ")\r"); cr.println("Connected to: " + this.GetHostname() + " (" + Bukkit.getServer().getName() + ")\r");
cr.println(ConsoleLogFormatter.ColorizeString(Bukkit.getServer().getMotd()).replaceAll("\n", "\r\n")); cr.println(ConsoleLogFormatter.ColorizeString(Bukkit.getServer().getMotd()).replaceAll("\n", "\r\n"));
cr.println("\r"); cr.println("\r");
cr.println("Type 'exit' to exit the shell." + "\r"); cr.println("Type 'exit' to exit the shell." + "\r");

View File

@ -1,18 +1,20 @@
package com.ryanmichela.sshd; package com.ryanmichela.sshd.jline;
import 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() { public 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

@ -0,0 +1,87 @@
# The IP addresses(s) the SSH server will listen on. Use a comma separated list for multiple addresses.
# Leave as "all" for all addresses.
ListenAddress: all
# The port the SSH server will listen on. Note that anything *below* 1024 will require you to run
# the whole minecraft server with elevated privileges (NOT RECOMMENDED).
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.
# To authorize a user to login with their public key, install their key using the
# OpenSSH authorized_keys file format in the authorized_users directory. Name the key
# 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.
# Type of hashing to use for the passwords below.
# Options are: PLAIN (insecure), bcrypt, pbkdf2, sha256
#
# You can use the console/in-game command `/mkpasswd [hash] PASSWORD` to
# generate a password hash string then copy it for your passwords below.
# You can also use `/mkpasswd help` to see what algorithms are supported.
PasswordType: bcrypt
# Associate each username with a password hash (or the password if the PasswordType is set to PLAIN)
Credentials:
# The defaults for any user who does not have a specific section.
# Specific user permissions override the $default pseudo-user.
$default:
# Whether they can read or write to the console
console: RW
# SFTP access for anyone.
sftp:
# Whether sftp is allowed at all.
enabled: true
# Whether to allow or deny by default
default: allow
# specific rules for directories
rules:
# Deny the SSHD config folder by default as an example.
"*SSHD/*":
readable: false
writeable: false
# Username (should match SSH key if using key-based authentication)
justasic:
# Password hash from /mkpasswd command
password: $2a$10$Oqk83FrypRrMF35EDeoQDuidJOQEWBE0joEQ7MJFi/Oeg26wQ3fm2
# Whether they can read, write, or have read/write permissions to console.
console: RW
# SFTP access for this user.
sftp:
# Whether SFTP is enabled for this user.
enabled: true
# Whether to deny access by default or allow access by default
default: allow
# Rules regarding their SFTP access.
# These rules are relative to the server root.
# This acts as a chroot for the server root.
# Each path can be an absolute path or a regular expression.
rules:
"/path/to/file":
# Whether the user can read the file over SFTP
readable: true
# Whether the user can write/modify the file over SFTP
writeable: true
"/path/to/regex/*":
readable: true
writeable: false
"/path/to/directory/":
readable: false
writeable: true
"/another/example/path":
readable: false
writeable: false

View File

@ -1,7 +1,8 @@
name: SSHD name: SSHD
version: ${project.version} version: ${project.version}
author: Ryan Michela, Haarolean, toxuin, Justin Crawford author: Ryan Michela, Haarolean, toxuin, Justin Crawford, Zachery Coleman
main: com.ryanmichela.sshd.SshdPlugin main: com.ryanmichela.sshd.SshdPlugin
load: STARTUP
commands: commands:
mkpasswd: mkpasswd:
description: Make a SSHD password hash description: Make a SSHD password hash

View File

@ -1,13 +1,13 @@
Spigot-SSHD Minecraft-SSHD
=========== ===========
[![Build Status](https://travis-ci.org/Justasic/Spigot-SSHD.svg?branch=master)](https://travis-ci.org/Justasic/Spigot-SSHD) [![GitHub license](https://img.shields.io/github/license/Justasic/Minecraft-SSHD)](https://github.com/Justasic/Minecraft-SSHD/blob/master/LICENSE)
[![Release](https://img.shields.io/github/release/Justasic/Spigot-SSHD.svg?label=Release&maxAge=60)](https://github.com/Justasic/Spigot-SSHD/releases/latest)
[![GitHub license](https://img.shields.io/github/license/Justasic/Spigot-SSHD)](https://github.com/Justasic/Spigot-SSHD/blob/master/LICENSE)
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. <img align="left" width="140" height="140" src="docs/ssh_logo.png?raw=true" hspace="5" vspace="5" alt="diskover"><br/>
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. **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 Minecraft-SSHD!**
Minecraft-SSHD securely exposes your Minecraft admin console and the server filesystem using the SSH protocol - the same protocol that serves as the secure foundation for nearly all remote server administration.<br/>
- Compatible with all ssh clients, regardless of operating system. - Compatible with all ssh clients, regardless of operating system.
- Remotely view your server log in real-time. - Remotely view your server log in real-time.
@ -18,7 +18,7 @@ SSHD securely exposes your Spigot admin console using the SSH protocol - the sam
- Run Spigot without using screen or tmux (by adding `-noconsole`) - Run Spigot without using screen or tmux (by adding `-noconsole`)
- Remotely script your server by issuing one-off console commands with ssh. - Remotely script your server by issuing one-off console commands with ssh.
### Why should I use SSHD? ### Why should I use Minecraft-SSHD?
- You are in a shared hosting environment that only gives you access to the - log files. - 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 want to share access to your server console, but don't want to give anybody access to the machine its running on.
@ -91,8 +91,11 @@ mkpasswd supports the following hash algorithms:
`sshd.mkpasswd` - Checks if the in-game user has access to run the mkpasswd command. `sshd.mkpasswd` - Checks if the in-game user has access to run the mkpasswd command.
SSHD uses cryptographic certificates or a secure username and password to verify remote access. Minecraft-SSHD uses cryptographic certificates or a secure username and password to verify remote access.
## Source Code ## Source Code
[Get the source on GitHub](https://github.com/Justasic/Spigot-SSHD "Source Code") [Get the source on GitHub](https://github.com/Justasic/Minecraft-SSHD "Source Code")
## Our Discord
https://discord.gg/DtrJFn

BIN
docs/ssh_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

133
docs/ssh_logo.svg Normal file
View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="800"
height="800"
viewBox="0 0 211.66666 211.66667"
version="1.1"
id="svg8"
sodipodi:docname="ssh_logo.svg"
inkscape:version="0.92.4 5da689c313, 2019-01-14">
<defs
id="defs2">
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter5273">
<feFlood
flood-opacity="1"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood5263" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite5265" />
<feGaussianBlur
in="composite1"
stdDeviation="3.5"
result="blur"
id="feGaussianBlur5267" />
<feOffset
dx="1"
dy="1"
result="offset"
id="feOffset5269" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite5271" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="375.64503"
inkscape:cy="367.3313"
inkscape:document-units="mm"
inkscape:current-layer="layer3"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1920"
inkscape:window-y="119"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Background"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-85.333343)">
<circle
style="fill:#4c4c4c;fill-opacity:1;stroke-width:0.30369404;filter:url(#filter5273)"
id="path4533"
cx="105"
cy="192.00002"
r="100" />
<circle
style="fill:#242424;fill-opacity:1;stroke-width:0.27332464;"
id="path4533-3"
cx="105"
cy="192.00002"
r="90" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Text"
style="opacity:0.98999999">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:145.88342285px;line-height:0;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.31260732"
x="130.4019"
y="91.524567"
id="text5759"
transform="scale(0.84637592,1.1815081)"><tspan
sodipodi:role="line"
id="tspan5757"
x="130.4019"
y="91.524567"
style="font-size:145.88342285px;line-height:0;fill:#ffffff;fill-opacity:1;stroke-width:0.31260732">_</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:145.88342285px;line-height:0;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;opacity:0.98999999;fill:#20bb00;fill-opacity:1;stroke:none;stroke-width:0.31260732"
x="39.53738"
y="118.43275"
id="text5759-2"
transform="scale(0.84637592,1.1815081)"><tspan
sodipodi:role="line"
id="tspan5757-5"
x="39.53738"
y="118.43275"
style="font-size:123.47222137px;line-height:0;fill:#20bb00;fill-opacity:1;stroke-width:0.31260732">&gt;</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

101
pom.xml
View File

@ -6,8 +6,12 @@
<groupId>com.ryanmichela</groupId> <groupId>com.ryanmichela</groupId>
<artifactId>sshd</artifactId> <artifactId>sshd</artifactId>
<version>1.3.6.1</version> <description>Minecraft-SSHD: The SSH daemon for Minecraft servers.</description>
<url>https://github.com/Justasic/Bukkit-SSHD/</url> <modules>
<module>Minecraft-SSHD-Bukkit</module>
</modules>
<version>2.1.0</version>
<url>https://git.limework.net/Limework/Minecraft-SSHD/</url>
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>
@ -25,37 +29,36 @@
<!-- License --> <!-- License -->
<licenses> <licenses>
<license> <license>
<name>GPL2</name> <name>Apache-2.0</name>
<url>http://www.gnu.org/licenses/gpl-2.0.html</url> <url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license> </license>
</licenses> </licenses>
<!-- Dependencies --> <!-- Dependencies -->
<dependencies> <dependencies>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.14.4-R0.1-SNAPSHOT</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId> <artifactId>sshd-core</artifactId>
<version>2.3.0</version> <version>2.9.0</version>
<scope>compile</scope> <scope>compile</scope>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.apache.sshd</groupId>
<artifactId>sshd-mina</artifactId> <artifactId>sshd-mina</artifactId>
<version>2.3.0</version> <version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-contrib</artifactId>
<version>2.9.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.apache.sshd</groupId>
<artifactId>sshd-common</artifactId> <artifactId>sshd-common</artifactId>
<version>2.3.0</version> <version>2.9.0</version>
<scope>compile</scope> <scope>compile</scope>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
@ -63,7 +66,7 @@
<dependency> <dependency>
<groupId>org.apache.sshd</groupId> <groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId> <artifactId>sshd-sftp</artifactId>
<version>2.3.0</version> <version>2.9.0</version>
</dependency> </dependency>
<dependency> <dependency>
@ -75,100 +78,48 @@
<dependency> <dependency>
<groupId>org.apache.mina</groupId> <groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId> <artifactId>mina-core</artifactId>
<version>2.1.3</version> <version>2.2.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
<version>1.7.28</version> <version>2.0.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId> <artifactId>slf4j-jdk14</artifactId>
<version>1.7.28</version> <version>2.0.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>jline</groupId> <groupId>jline</groupId>
<artifactId>jline</artifactId> <artifactId>jline</artifactId>
<version>2.12.1</version> <version>2.14.6</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId> <artifactId>log4j-core</artifactId>
<version>2.0</version> <version>2.20.0</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId> <artifactId>log4j-api</artifactId>
<version>2.1</version> <version>2.20.0</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
<version>1.10</version> <version>1.15</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<!-- Build --> <packaging>pom</packaging>
<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>
<include>motd.txt</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>${project.name}-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<!-- Compile plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
</project> </project>

View File

@ -1,99 +0,0 @@
package com.ryanmichela.sshd;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import java.util.Arrays;
import com.ryanmichela.sshd.Cryptography;
import com.ryanmichela.sshd.SshdPlugin;
class MkpasswdCommand implements CommandExecutor
{
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args)
{
String algoritm, password;
try
{
// Stupid bukkit, we have to concatenate the arguments together if they're using
// spaces in their passwords otherwise it won't be as strong as it should be.
algoritm = args[0];
password = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
}
catch (ArrayIndexOutOfBoundsException e)
{
// ignore it.
return false;
}
// If they're console, allow regardless.
if (!(sender instanceof Player))
{
if (label.equalsIgnoreCase("mkpasswd"))
{
try
{
// Dumb but whatever. Some people are really dense.
if (algoritm.equalsIgnoreCase("PLAIN"))
{
// I mean c'mon...
sender.sendMessage("Bro really? it's literally your unencrypted password...");
}
else if (algoritm.equalsIgnoreCase("pbkdf2"))
sender.sendMessage("Your hash: " + Cryptography.PBKDF2_HashPassword(password));
else if (algoritm.equalsIgnoreCase("bcrypt"))
sender.sendMessage("Your hash: " + Cryptography.BCrypt_HashPassword(password));
else if (algoritm.equalsIgnoreCase("sha256"))
sender.sendMessage("Your hash: " + Cryptography.SHA256_HashPassword(password));
else if (algoritm.equalsIgnoreCase("help"))
sender.sendMessage("Supported hash algorithms: pbkdf2, bcrypt, sha256, plain");
else
return false;
}
catch (Exception e)
{
// We're console, just print the stack trace.
e.printStackTrace();
return false;
}
return true;
}
}
else
{
Player player = (Player) sender;
if (label.equalsIgnoreCase("mkpasswd"))
{
try
{
if (player.hasPermission("sshd.mkpasswd"))
{
// Dumb but whatever. Some people are really dense.
if (algoritm.equalsIgnoreCase("PLAIN"))
sender.sendMessage(password);
else if (algoritm.equalsIgnoreCase("pbkdf2"))
sender.sendMessage(Cryptography.PBKDF2_HashPassword(password));
else if (algoritm.equalsIgnoreCase("bcrypt"))
sender.sendMessage(Cryptography.BCrypt_HashPassword(password));
else if (algoritm.equalsIgnoreCase("sha256"))
sender.sendMessage(Cryptography.SHA256_HashPassword(password));
else
return false;
}
}
catch (Exception e)
{
// since this is a player, send a failure message
sender.sendMessage("An error occured, please check console.");
e.printStackTrace();
return false;
}
return true;
}
}
return false;
}
}

View File

@ -1,105 +0,0 @@
package com.ryanmichela.sshd;
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.subsystem.sftp.SftpSubsystemFactory;
import org.bukkit.plugin.java.JavaPlugin;
import com.ryanmichela.sshd.ConsoleShellFactory;
import com.ryanmichela.sshd.MkpasswdCommand;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Collections;
import java.util.logging.Level;
/**
* Copyright 2013 Ryan Michela
*/
public
class SshdPlugin extends JavaPlugin
{
private SshServer sshd;
public static SshdPlugin instance;
@Override public void onLoad()
{
saveDefaultConfig();
File authorizedKeys = new File(getDataFolder(), "authorized_keys");
if (!authorizedKeys.exists())
authorizedKeys.mkdirs();
try
{
File motd = new File(getDataFolder(), "motd.txt");
if (!motd.exists())
{
InputStream link = (getClass().getResourceAsStream("/motd.txt"));
Files.copy(link, motd.getAbsoluteFile().toPath());
}
}
catch (IOException e)
{
e.printStackTrace();
}
// Don't go any lower than INFO or SSHD will cause a stack overflow exception.
// SSHD will log that it wrote bites to the output stream, which writes
// bytes to the output stream - ad nauseaum.
getLogger().setLevel(Level.INFO);
}
@Override public void onEnable()
{
instance = this;
sshd = SshServer.setUpDefaultServer();
sshd.setPort(getConfig().getInt("Port", 1025));
String host = getConfig().getString("ListenAddress", "all");
sshd.setHost(host.equals("all") ? null : host);
File hostKey = new File(getDataFolder(), "hostkey");
File authorizedKeys = new File(getDataFolder(), "authorized_keys");
sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKey.toPath()));
sshd.setShellFactory(new ConsoleShellFactory());
sshd.setPasswordAuthenticator(new ConfigPasswordAuthenticator());
sshd.setPublickeyAuthenticator(new PublicKeyAuthenticator(authorizedKeys));
if (getConfig().getBoolean("EnableSFTP"))
{
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
sshd.setFileSystemFactory(
new VirtualFileSystemFactory(FileSystems.getDefault().getPath(getDataFolder().getAbsolutePath()).getParent().getParent()));
}
this.getCommand("mkpasswd").setExecutor(new MkpasswdCommand());
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

@ -1,41 +0,0 @@
# The IP addresses(s) the SSH server will listen on. Use a comma separated list for multiple addresses.
# 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.
# To authorize a user to login with their public key, install their key using the
# OpenSSH authorized_keys file format in the authorized_users directory. Name the key
# 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.
# Type of hashing to use for the passwords below.
# Options are: PLAIN (insecure), bcrypt, pbkdf2, sha256
#
# You can use the console/in-game command `/mkpasswd [hash] PASSWORD` to
# generate a password hash string then copy it for your passwords below.
# You can also use `/mkpasswd help` to see what algorithms are supported.
PasswordType: bcrypt
# Associate each username with a password hash (or the password if the PasswordType is set to PLAIN)
Credentials:
# user1: password1
# user2: password2