aboutsummaryrefslogtreecommitdiffgithub
diff options
context:
space:
mode:
authorAustin Adams <screamingmoron@gmail.com>2013-07-05 22:02:18 -0400
committerAustin Adams <screamingmoron@gmail.com>2013-07-05 22:02:18 -0400
commita21cdd662017ed94403533345ffddb23309d07f3 (patch)
tree744f1102c5534fcc268d77cf0a9a9857883dc9cc
parent0377a9a38876e5b21c4de29cf3851ae6aea87541 (diff)
downloadmccmd-a21cdd662017ed94403533345ffddb23309d07f3.tar.gz
mccmd-a21cdd662017ed94403533345ffddb23309d07f3.tar.xz
mccmd 0.3: new C client, better server logging
- server now logs each command along with the client's ip address and port - made MessageReciever identify itself as 'mccmd@clienthost:clientport' instead of just 'mccmd' - moved ConnectionHandler out of SocketListenerThread.java and into its own file, ConnectionThread.java. Sort of a hack to make writing the Makefile easier (SocketListenerThread.java -> SocketListenerThread.class + SocketListenerThread$ConnectionHandler.class is not an easy thing to predict in a Makefile), but it improves readability so whatever. - replaced crappy python client with a shiny new C client - systemd unit is no longer included in the arch PKGBUILD (tweak it and put it in /etc/systemd/system/ yourself!) - swapped build system from kludgey shell scripts to Makefiles - update README to reflect changes
-rw-r--r--Makefile38
-rw-r--r--PKGBUILD27
-rw-r--r--README.md89
-rw-r--r--mccmd.c481
-rwxr-xr-xmccmd.py179
-rwxr-xr-xplugin/build.sh24
-rw-r--r--plugin/config.yml (renamed from plugin/src/config.yml)0
-rw-r--r--plugin/io/github/ausbin/mccmd/Config.java (renamed from plugin/src/io/github/ausbin/mccmd/Config.java)2
-rw-r--r--plugin/io/github/ausbin/mccmd/ConnectionThread.java152
-rw-r--r--plugin/io/github/ausbin/mccmd/Mccmd.java (renamed from plugin/src/io/github/ausbin/mccmd/Mccmd.java)1
-rw-r--r--plugin/io/github/ausbin/mccmd/MessageReceiver.java (renamed from plugin/src/io/github/ausbin/mccmd/MessageReceiver.java)9
-rw-r--r--plugin/io/github/ausbin/mccmd/SocketListenerThread.java105
-rw-r--r--plugin/plugin.yml (renamed from plugin/src/plugin.yml)2
-rw-r--r--plugin/src/io/github/ausbin/mccmd/SocketListenerThread.java209
14 files changed, 843 insertions, 475 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..da0f8d6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,38 @@
+# client (C) stuff
+CFLAGS =
+
+# java misery (plugin)
+JAR = jar
+JAVAC = javac
+PLUGIN = plugin
+NAMESPACE = io/github/ausbin/mccmd
+CLASSES = $(wildcard $(PLUGIN)/$(NAMESPACE)/*.java)
+COMPILED = $(CLASSES:%.java=%.class)
+CONFIG = $(wildcard $(PLUGIN)/*.yml)
+CURL = curl
+BUKKITJAR = bukkit.jar
+BUKKITURL = http://dl.bukkit.org/downloads/bukkit/get/02168_1.5.2-R1.0/bukkit.jar
+
+all: mccmd Mccmd.jar
+
+mccmd: mccmd.c
+ $(CC) -lreadline $(CFLAGS) -o $@ $<
+
+# retrieve a compatible copy of the bukkit api
+# sort of a hack, but it works. if you don't want to use this,
+# pass BUKKITJAR=/path/to/my/bukkit.jar as an argument to make.
+$(BUKKITJAR):
+ $(CURL) -Lo $@ $(BUKKITURL)
+
+# each javac call requires a JVM spinup, so cherrypicking and calling
+# javac on every modified source file is actually slower than using a
+# single javac call to rebuild everything
+$(COMPILED): $(CLASSES) $(BUKKITJAR)
+ $(JAVAC) -classpath $(BUKKITJAR) $(JAVACFLAGS) $(CLASSES)
+
+Mccmd.jar: $(COMPILED) $(CONFIG)
+ $(JAR) cf $@ $(CONFIG:$(PLUGIN)/%=-C $(PLUGIN) %) \
+ $(COMPILED:$(PLUGIN)/%=-C $(PLUGIN) %)
+
+clean:
+ rm -f $(BUKKITJAR) $(COMPILED) mccmd Mccmd.jar
diff --git a/PKGBUILD b/PKGBUILD
index b3463f4..372e09a 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -1,17 +1,26 @@
# Maintainer: Austin Adams <screamingmoron@gmail.com>
pkgname=mccmd
-pkgver=0.1
+pkgver=0.0.0
pkgrel=1
-pkgdesc="mccmd client and systemd service"
+pkgdesc="mccmd client"
arch=("any")
url="https://github.com/ausbin/mccmd/"
-license=('MIT')
-depends=("python")
-optdepends=("systemd: for using the bundled service file")
-source=("http://206.253.166.8/~austin/builds/mccmd-${pkgver}.tar.xz")
-md5sums=("d7f05fdaec45ad4efff671a8dcbc4a58")
+license=("MIT")
+depends=("readline")
+source=("$pkgname::git+https://github.com/ausbin/mccmd.git")
+md5sums=('SKIP')
+
+pkgver () {
+ cd $pkgname
+ git describe --always
+}
+
+build () {
+ cd $pkgname
+ make mccmd
+}
package() {
- install -Dm 644 $srcdir/minecraft.service $pkgdir/usr/lib/systemd/system/minecraft.service
- install -Dm 755 $srcdir/mccmd.py $pkgdir/usr/bin/mccmd
+ cd $pkgname
+ install -Dm 755 $srcdir/mccmd $pkgdir/usr/bin/mccmd
}
diff --git a/README.md b/README.md
index 9d2d7ef..fc6c447 100644
--- a/README.md
+++ b/README.md
@@ -7,31 +7,27 @@ It currently consists of:
1. a systemd service file for craftbukkit
2. a java craftbukkit plugin implementing the [mccmd protocol][]
-3. a python client for the mccmd protocol
+3. a c client for the mccmd protocol
All components are under the [MIT License][].
-An Arch PKGBUILD for the client and systemd unit can be found
-[here][PKGBUILD]. A source tarball is [here][sourcetarball].
-
systemd unit
------------
-By default, `minecraft.service` is set up to run craftbukkit in
-`/srv/minecraft/` as the user "minecraft" and the group "minecraft".
+`minecraft.service` is a template systemd unit for craftbukkit. While
+it is a complete and working service file, you should modify it to your own
+requirements. Keep in mind, though, that running the minecraft server as its
+own user and group in its own directory is probably a good idea.
-You should either create those or modify the content of the unit once
-it's in `/etc/systemd/system/`.
+Like all custom units, `minecraft.service` should live in
+`/etc/systemd/system/`.
bukkit plugin
-------------
-You can download a build of the mccmd bukkit plugin from
-[here][plugindownload], but if you want to build it yourself,
-try this (you'll have to find a suitable bukkit build for yourself, of course):
+You can build the plugin with the bundled Makefile, but a (possibly outdated)
+prebuilt version is available [here][plugindownload] if you're feeling lazy.
$ git clone git://github.com/ausbin/mccmd.git mccmd
- $ cd mccmd/plugin/
- $ curl -o bukkit.jar http://repo.bukkit.org/content/groups/public/org/bukkit/bukkit/$bukkitrel/$bukkitbuild.jar
- $ CLASSPATH=bukkit.jar ./build.sh
+ $ make Mccmd.jar
The [yaml][] config file is pretty simple. If a config file doesn't
already exist, the defaults are copied over when the plugin is enabled.
@@ -41,39 +37,34 @@ you want.
client
------
-The client is a crappy python 3 script that provides an interactive
-prompt for sending commands to a server (if both stdin and stdout
-are ttys). It converts [Minecraft chat control sequences][controlseq]
-to [ANSI escape sequences][]. For a list of the conversions, [look at
-the code](https://github.com/ausbin/mccmd/blob/master/mccmd.py).
-
- $ ./mccmd.py --help
- usage: mccmd.py [-h] [-p] [-c] [host] [port]
-
- send commands to a craftbukkit server via the mccmd plugin and/or convert or
- remove minecraft chat control characters.
-
- positional arguments:
- host host to connect to (defaults to localhost)
- port port to connect to on 'host' (defaults to 5454)
-
- optional arguments:
- -h, --help show this help message and exit
- -p, --process convert or remove minecraft chat control characters in stdin
- instead of connecting to a server (nullifies all other
- options except for -c/--nocolor)
- -c, --nocolor discard minecraft chat control sequences instead of
- converting them directly to ansi control sequences
+The client is a very simple C program that reads lines from stdin and sends
+them to a mccmd server. If both stdin and stdout are ttys, it provides a cute
+little interactive prompt with readline support.
+By default in an interactive session, minecraft chat formatting (colors and
+styles) are converted to [ANSI escape sequences][].
+For a list of the conversions, [look at the
+code](https://github.com/ausbin/mccmd/blob/master/mccmd.c).
+ $ git clone git://github.com/ausbin/mccmd.git mccmd
+ $ make mccmd
+ $ ./mccmd -h
+ usage: mccmd [-r|-i|-c|-h] [host [port]]
+
+ send commands to a minecraft server.
+ (default host is localhost, default port is 5454)
+
+ -r remove minecraft chat formatting
+ (the default when not in a tty)
+ -i leave minecraft chat formatting intact
+ -c convert minecraft chat formatting into ansi
+ escape sequences (the default in a tty)
+ -h show this message
+
https://github.com/ausbin/mccmd
-The client can also be used to process minecraft chat control sequences via
-the `--process` option. The `--nocolor` flag can be paired with it,
-which is handy for cleaning out text that was formatted for minecraft
-chat.
-
- $ echo "§d§lpretty pink so pretty" | ./mccmd.py --process --nocolor
- pretty pink so pretty
+The client requires a compiler that supports [C89][] and a system with
+[POSIX][] and [readline][]. If you're on Arch, just download the [PKGBUILD][]
+into a fresh directory and run `makepkg`.
protocol
--------
@@ -116,8 +107,8 @@ For example, sending `help\n` produces:
Notice that 524 is in text form and measures the number of bytes left
*after* the newline that terminates the header. Also notice that
[minecraft chat control sequences][controlseq] are intact. (The bundled
-[client][] converts them to [ANSI escape sequences][] automatically by
-default)
+[client][] converts them to [ANSI escape sequences][] by default in
+interactive sessions)
[systemd]: http://freedesktop.org/wiki/Software/systemd/
[mccmd protocol]: #protocol
@@ -125,9 +116,11 @@ default)
[controlseq]: http://wiki.vg/Chat#Control_Sequences
[UTF-8]: http://utf-8.com/
[ANSI escape sequences]: https://en.wikipedia.org/wiki/ANSI_escape_code
-[plugindownload]: http://206.253.166.8/~austin/builds/mccmd.jar
+[plugindownload]: http://206.253.166.8/~austin/builds/Mccmd.jar
[CommandSender]: http://jd.bukkit.org/dev/apidocs/org/bukkit/command/CommandSender.html
+[C89]: https://en.wikipedia.org/wiki/ANSI_C
+[POSIX]: https://en.wikipedia.org/wiki/POSIX
+[readline]: http://cnswww.cns.cwru.edu/php/chet/readline/rltop.html
[yaml]: https://en.wikipedia.org/wiki/YAML
[PKGBUILD]: https://raw.github.com/ausbin/mccmd/master/PKGBUILD
-[sourcetarball]: http://206.253.166.8/~austin/builds/mccmd.tar.xz
[MIT License]: https://en.wikipedia.org/wiki/MIT_License
diff --git a/mccmd.c b/mccmd.c
new file mode 100644
index 0000000..bb8daa3
--- /dev/null
+++ b/mccmd.c
@@ -0,0 +1,481 @@
+/* Copyright (c) 2013 Austin Adams
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#include <netdb.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+
+/* the response parser's current location.
+ in a response, the header is first line. it consists the length of the body
+ in the form of a series of ASCII digits terminated by a newline.
+ the body contains the output of the command included in the request. */
+enum location {HEAD, BODY};
+/* describes the state of a formatting style.
+ NA is n/a or not applicable. used when a feature is not to be changed.
+ OFF disables a feature (e.g. no bold), and ON enables it (e.g. italics). */
+enum style {NA = -1, OFF, ON};
+/* ANSI escape sequence colors. taken from
+ https://wikipedia.org/wiki/ANSI_escape_code#Colors */
+enum color {ANY = -1, BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE};
+/* the next byte in a minecraft chat control sequence:
+ don't look for the next byte at all, leading byte in § (\xc2), continuation
+ byte in § (\xa7), or format letter (0-f) (respectively). */
+enum seqbyte {IGNORE = -1, LEADING, CONTINUATION, LETTER};
+/* what to do with minecraft chat control sequences
+ convert to ansi escape sequences, remove them entirely, or leave them */
+enum seqaction {CONVERT, REMOVE, INTACT};
+
+/* describes the current state of or change in formatting.
+ when used to represent the current state of formatting during parsing,
+ all values are either OFF or ON. For formatting deltas, however, each
+ value is set to NA (no change), OFF (disable the feature), or ON (enable) */
+struct format {
+ enum style random;
+ enum style bold;
+ enum style strikethru;
+ enum style underline;
+ enum style italic;
+ enum color color; /* the current ansi color */
+};
+/* describes a minecraft chat formatting letter */
+struct chatcode {
+ const char letter; /* one of 0123456789abcdefklmnor */
+ const struct format delta; /* the resulting change in formatting */
+};
+
+/* the maximum amount of a response to recv() at once */
+const size_t recvsize = 1024;
+
+/* size of each chunk of memory allocated while reading input lines from a
+ non-tty */
+const size_t lineincr = 128;
+
+const char *const program = "mccmd";
+const char *const defaultport = "5454";
+const char *const prompt = "> ";
+
+/* symbol that starts a minecraft chat control sequence */
+const char *const seqsymbol = "§";
+
+/* map of minecraft formatting letters to their formatting changes
+ gleaned from http://wiki.vg/Chat#Control_Sequences */
+const struct chatcode chatcodes[] = {
+ {'0', {OFF, OFF, OFF, OFF, OFF, BLACK}},
+ {'1', {OFF, OFF, OFF, OFF, OFF, BLUE}},
+ {'2', {OFF, OFF, OFF, OFF, OFF, GREEN}},
+ {'3', {OFF, OFF, OFF, OFF, OFF, CYAN}},
+ {'4', {OFF, OFF, OFF, OFF, OFF, RED}},
+ {'5', {OFF, OFF, OFF, OFF, OFF, MAGENTA}},
+ {'6', {OFF, OFF, OFF, OFF, OFF, YELLOW}},
+ {'7', {OFF, OFF, OFF, OFF, OFF, WHITE}},
+ {'8', {OFF, OFF, OFF, OFF, OFF, BLACK}},
+ {'9', {OFF, OFF, OFF, OFF, OFF, BLUE}},
+ {'a', {OFF, OFF, OFF, OFF, OFF, GREEN}},
+ {'b', {OFF, OFF, OFF, OFF, OFF, CYAN}},
+ {'c', {OFF, OFF, OFF, OFF, OFF, RED}},
+ {'d', {OFF, OFF, OFF, OFF, OFF, MAGENTA}},
+ {'e', {OFF, OFF, OFF, OFF, OFF, YELLOW}},
+ {'f', {OFF, OFF, OFF, OFF, OFF, WHITE}},
+ {'k', {ON, NA, NA, NA, NA, ANY}}, /* randomness */
+ {'l', {NA, ON, NA, NA, NA, ANY}}, /* bold */
+ {'m', {NA, NA, ON, NA, NA, ANY}}, /* strikethrough */
+ {'n', {NA, NA, NA, ON, NA, ANY}}, /* underline */
+ {'o', {NA, NA, NA, NA, ON, ANY}}, /* italic */
+ {'r', {OFF, OFF, OFF, OFF, OFF, WHITE}} /* reset */
+};
+
+void parseargs (int, char **, char **, char **, enum seqaction *);
+int getsock (char *, char *);
+char *line (int);
+void printresponse (int, enum seqaction);
+
+int main (int argc, char **argv) {
+ int sock, interactive;
+ char *lineread, *host, *port;
+ enum seqaction seqaction;
+
+ /* try to find out if we're being piped or used interactively */
+ interactive = isatty(fileno(stdin)) && isatty(fileno(stdout));
+
+ /* default to the loopback address */
+ host = NULL;
+ /* cast to get rid of warning about defaultport being a constant pointer
+ to a constant character (whereas port is just a char *) */
+ port = (char *) defaultport;
+ /* convert minecraft chat formatting to ansi escape sequences if we're
+ running interactively, otherwise (if we're being piped) remove them */
+ seqaction = interactive? CONVERT : REMOVE;
+
+ /* update the args struct according to the arguments passed into the
+ program (or exit if an invalid option or -h was specified) */
+ parseargs(argc, argv, &host, &port, &seqaction);
+
+ sock = getsock(host, port);
+
+ while ((lineread = line(interactive)) != NULL) {
+ send(sock, lineread, strlen(lineread), 0);
+ free(lineread);
+
+ /* send terminating newline */
+ send(sock, (void *)"\n", 1, 0);
+
+ printresponse(sock, seqaction);
+ }
+
+ /* clear out the prompt so that $PS1 will be on a new line */
+ if (interactive)
+ putchar('\n');
+
+ close(sock);
+
+ return EXIT_SUCCESS;
+}
+
+/* XXX transition towards function return values
+ (this is sort of a hack) */
+/* crash and print either a custom message or the error message associated
+ with ERRNO */
+void die (const char *msg) {
+ if (msg == NULL)
+ perror(program);
+ else
+ fprintf(stderr, "%s: %s\n", program, msg);
+
+ exit(EXIT_FAILURE);
+}
+
+/* print usage to the specified output stream */
+/* XXX find a way to integrate this into die() */
+void usage (FILE *stream) {
+ fprintf(stream, "usage: %s [-r|-i|-c|-h] [host [port]]\n"
+ "\n"
+ "send commands to a minecraft server.\n"
+ "(default host is localhost, default port is %s)\n"
+ "\n"
+ "-r remove minecraft chat formatting\n"
+ " (the default when not in a tty)\n"
+ "-i leave minecraft chat formatting intact\n"
+ "-c convert minecraft chat formatting into ansi\n"
+ " escape sequences (the default in a tty)\n"
+ "-h show this message\n"
+ "\n"
+ "https://github.com/ausbin/mccmd\n"
+ , program, defaultport);
+}
+
+/* change the arguments passed into the program into a more usable struct */
+void parseargs (int argc, char **argv, char **host, char **port,
+ enum seqaction *seqaction) {
+ int c;
+
+ while ((c = getopt(argc, argv, "rich")) != -1) {
+ switch (c) {
+ case 'r':
+ *seqaction = REMOVE;
+ break;
+ case 'i':
+ *seqaction = INTACT;
+ break;
+ case 'c':
+ *seqaction = CONVERT;
+ break;
+ case 'h':
+ /* XXX this is ugly */
+ usage(stdout);
+ exit(EXIT_SUCCESS);
+ break;
+ case '?':
+ usage(stderr);
+ exit(EXIT_FAILURE);
+ break;
+ }
+ }
+
+ /* there should not be more than 2 positional arguments */
+ if (argc-optind > 2) {
+ usage(stderr);
+ exit(EXIT_FAILURE);
+ }
+
+ while (optind < argc)
+ if (*host == NULL)
+ *host = argv[optind++];
+ else
+ *port = argv[optind++];
+}
+
+/* find and return a socket fd connect()'ed to host:port */
+int getsock (char *host, char *port) {
+ int status, sock;
+ struct addrinfo gaiargs, *result, *rp;
+
+ /* set all of the values in the struct to pass to getaddrinfo() to NULL
+ (required according to the manpage) */
+ memset(&gaiargs, 0, sizeof (struct addrinfo));
+ /* allow either IPv4 or IPv6 sockets */
+ gaiargs.ai_family = AF_UNSPEC;
+ gaiargs.ai_socktype = SOCK_STREAM; /* tcp */
+
+ status = getaddrinfo(host, port, &gaiargs, &result);
+
+ /* bail if there were any complaints from getaddrinfo() */
+ if (status != 0)
+ die(gai_strerror(status));
+
+ /* look for a working address in the "list" from getaddrinfo() */
+ for (rp = result; rp != NULL; rp = rp->ai_next) {
+ /* try to make a socket with this address */
+ sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+
+ /* if it didn't work, try the next one */
+ if (sock == -1)
+ continue;
+
+ /* if connecting to the socket succeeds, stop trying other
+ addresses */
+ if (connect(sock, rp->ai_addr, rp->ai_addrlen) != -1)
+ break;
+ else
+ /* otherwise, close the socket and try the next one */
+ close(sock);
+ }
+
+ /* XXX what if (somehow) 'result' was a null pointer even though
+ getaddrinfo() returned a zero exit status? */
+ /* if all of the addresses we tried failed, print the message associated
+ with ERRNO (it probably has more information about the latest failure) */
+ if (rp == NULL)
+ die(NULL);
+
+ freeaddrinfo(result);
+
+ return sock;
+}
+
+/* read a line from stdin (used if the session in non-interactive) */
+char *feedline (void) {
+ char c, *result;
+ int i, bufsize;
+
+ bufsize = 0;
+ result = NULL;
+
+ for (i = 0; (c = getchar()) != EOF && c != '\n'; ++i) {
+ /* if the amount of memory we've read is larger than the memory we've
+ allocated, allocate LINEINCR more memory */
+ if (i == bufsize)
+ result = realloc(result, (bufsize += lineincr));
+
+ result[i] = c;
+ }
+
+ /* mimic the way readline handles blank lines:
+ - if the line was *just* EOF, return a NULL pointer
+ - if the line had content followed by EOF, return the content
+ - if the line had content followed by a newline, return the content
+ - if the line was *just* a newline, return a pointer to a single \0 */
+ if (i == 0) {
+ if (c == EOF)
+ return NULL;
+ else if (c == '\n')
+ result = malloc(sizeof (char));
+ }
+
+ result[i] = '\0';
+
+ return result;
+}
+
+/* abstraction layer over readline() (for interactive sessions) and feedline()
+ (for piping or whatever - no keyboard shortcuts or prompts or anything) */
+char *line (int interactive) {
+ if (interactive) {
+ char *lineread;
+
+ lineread = readline(prompt);
+
+ /* if the line wasn't blank or EOF'd, add it to the history */
+ if (lineread != NULL && lineread[0] != '\0')
+ add_history(lineread);
+
+ return lineread;
+ } else {
+ return feedline();
+ }
+}
+
+/* return the change in formatting that would occur when §{letter} is
+ specified */
+const struct format *getdelta (const char letter) {
+ int i;
+
+ for (i = 0; i < (sizeof chatcodes / sizeof (struct chatcode)); i++)
+ if (chatcodes[i].letter == letter)
+ return &chatcodes[i].delta;
+
+ return NULL;
+}
+
+/* output an ansi control sequence from an integer */
+void putansi (int code) {
+ printf("\033[%im", code);
+}
+
+/* print an ansi escape sequence to stdout for every effect that needs to be
+ enabled or disabled. */
+void adjuststyle (enum style *from, const enum style *to, int ansicodeon) {
+ /* if *to has anything to change about the current formatting, update
+ both what the user sees and our internal representation */
+ if (*to != NA && *from != *to) {
+ if (*from == OFF && *to == ON)
+ putansi(ansicodeon);
+ else if (*from == ON && *to == OFF)
+ putansi(20 + ansicodeon); /* the code to disable an ansi effect is
+ 20 plus the code used to enable it */
+ *from = *to;
+ }
+}
+
+/* update the current color according to *to both internally and in
+ the terminal.*/
+void adjustcolor (enum color *from, const enum color *to) {
+ if (*to != ANY && *from != *to) {
+ putansi(30 + *to);
+ *from = *to;
+ }
+}
+
+/* change the current color and effects of text in stdout as stated in delta */
+void adjustformat (struct format *current, const struct format *delta) {
+ adjuststyle(&current->random, &delta->random, 5);
+ adjuststyle(&current->bold, &delta->bold, 1);
+ adjuststyle(&current->strikethru, &delta->strikethru, 9);
+ adjuststyle(&current->underline, &delta->underline, 4);
+ adjuststyle(&current->italic, &delta->italic, 3);
+
+ adjustcolor(&current->color, &delta->color);
+}
+
+/* de-absorb the parts of § we haven't printed.
+ called whenever an attempt at parsing a minecraft chat control sequence
+ is truncated or fails (e.g. §x shouldn't print x, §x -> §x is more chill) */
+void deabsorb (int nextseqbyte) {
+ int i;
+
+ for (i = LEADING; i < nextseqbyte; ++i)
+ putchar(seqsymbol[i]);
+}
+
+/* recv response and print output to sent command */
+void printresponse (int sock, enum seqaction seqaction) {
+ char *readed; /* the way all cool people say 'read' */
+ int location, nextseqbyte, i, bodylen, bodylenread, msglen;
+ struct format current;
+ const struct format *delta;
+
+ /* allocate memory for the response */
+ readed = malloc(recvsize);
+ /* assume the current state of the terminal is peaceful and happy */
+ memcpy(&current, getdelta('r'), sizeof (struct format));
+ /* our grand parsing quest will start in the response header */
+ location = HEAD;
+ bodylen = 0;
+ bodylenread = 0;
+
+ if (seqaction == INTACT)
+ nextseqbyte = IGNORE;
+ else
+ nextseqbyte = LEADING;
+
+ /* recieve RECVSIZE until we're in the body and we've read the amount of
+ the body specified in the header */
+ for (; !(location == BODY && bodylenread >= bodylen); i++) {
+ msglen = recv(sock, readed, recvsize, 0);
+
+ /* the server closed the connection peacefully, but before we could
+ read the header and the length of body as specified in the header */
+ if (msglen == 0)
+ die("connection terminated prematurely");
+ else if (msglen < 0)
+ die(NULL); /* print the errno message */
+
+ /* read the response header */
+ for (i = 0; location == HEAD && i < msglen; i++) {
+ /* start reading the body when we encounter a newline.
+ if a newline was the first thing we encountered, complain */
+ if (readed[i] == '\n' && i > 0)
+ location = BODY;
+ else {
+ if (!(readed[i] >= '0' && readed[i] <= '9'))
+ die("invalid character in response header");
+
+ bodylen *= 10; /* 45 -> 450 */
+ bodylen += (readed[i]-'0'); /* 450 -> 454 */
+ }
+ }
+
+ bodylenread += msglen-i;
+
+ /* read the body (pick up where the header parser left off */
+ for (; i < msglen; i++) {
+ /* if we're looking for the leading byte and we encounter it,
+ "absorb" it and start looking for the continuation byte */
+ if (nextseqbyte == LEADING && readed[i] == seqsymbol[nextseqbyte])
+ nextseqbyte = CONTINUATION;
+ /* if, on the other hand, we're looking for the continuation byte
+ and we find it, start looking for the letter (0-f) */
+ else if (nextseqbyte == CONTINUATION && readed[i] == seqsymbol[nextseqbyte])
+ nextseqbyte = LETTER;
+ /* finally, if we're looking for a letter and we find a valid one,
+ start looking for the leading byte again and adjust formatting
+ accordingly if we're supposed to */
+ else if (nextseqbyte == LETTER && (delta = getdelta(readed[i])) != NULL) {
+ nextseqbyte = LEADING;
+
+ if (seqaction == CONVERT)
+ adjustformat(&current, delta);
+ /* if all of those tests failed, spit out any of the parts of §
+ we've absorbed so far (if any), output the character in
+ question and start looking for the leading byte again */
+ } else {
+ deabsorb(nextseqbyte);
+
+ if (nextseqbyte > LEADING)
+ nextseqbyte = LEADING;
+
+ putchar(readed[i]);
+ }
+ }
+ }
+
+ free(readed);
+
+ /* again, choke out any parts of § we've absorbed */
+ deabsorb(nextseqbyte);
+
+ /* return the terminal to its happy peaceful state */
+ adjustformat(&current, getdelta('r'));
+}
diff --git a/mccmd.py b/mccmd.py
deleted file mode 100755
index b198979..0000000
--- a/mccmd.py
+++ /dev/null
@@ -1,179 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (c) 2013 Austin Adams
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-import re
-import sys
-from argparse import ArgumentParser
-
-def ansify (*codez) :
- result = ""
- for c in codez :
- result += "\x1b[{0}m".format(c)
- return result
-
-ctlseq = re.compile(r"§([0123456789abcdefklmnor])")
-mapping = {
- # no bold, no italic, no underline, no blink, not crossed-out, color
- "0": ansify(22, 23, 24, 25, 29, 30), # black -> black
- "1": ansify(22, 23, 24, 25, 29, 34), # dark blue -> blue
- "2": ansify(22, 23, 24, 25, 29, 32), # dark green -> green
- "3": ansify(22, 23, 24, 25, 29, 36), # dark cyan -> cyan
- "4": ansify(22, 23, 24, 25, 29, 31), # dark red -> red
- "5": ansify(22, 23, 24, 25, 29, 35), # purple -> magenta
- "6": ansify(22, 23, 24, 25, 29, 33), # gold -> yellow
- "7": ansify(22, 23, 24, 25, 29, 37), # gray -> white
- "8": ansify(22, 23, 24, 25, 29, 30), # dark gray -> black
- "9": ansify(22, 23, 24, 25, 29, 34), # blue -> blue
- "a": ansify(22, 23, 24, 25, 29, 32), # bright green -> green
- "b": ansify(22, 23, 24, 25, 29, 36), # cyan -> cyan
- "c": ansify(22, 23, 24, 25, 29, 31), # red -> red
- "d": ansify(22, 23, 24, 25, 29, 35), # pink -> magenta
- "e": ansify(22, 23, 24, 25, 29, 33), # yellow -> yellow
- "f": ansify(22, 23, 24, 25, 29, 37), # white -> white
- "k": ansify(6), # randomness -> fast blinking
- "l": ansify(1), # bold -> bold
- "m": ansify(9), # strike-thru -> crossed-out (rare)
- "n": ansify(4), # underline -> underline
- "o": ansify(3), # italic -> italic (semi-rare)
- "r": ansify(22, 23, 24, 25, 29, 37), # reset -> reset
-}
-parser = ArgumentParser(description="send commands to a craftbukkit server "
- "via the mccmd plugin and/or convert "
- "or remove minecraft chat control "
- "characters.",
- epilog="https://github.com/ausbin/mccmd")
-
-parser.add_argument("-p", "--process", action="store_true",
- help="convert or remove minecraft chat control "
- "characters in stdin instead of connecting to a "
- "server (nullifies all other options except for "
- "-c/--nocolor)")
-parser.add_argument("host", nargs="?", default="localhost",
- help="host to connect to (defaults to localhost)")
-parser.add_argument("port", nargs="?", type=int, default=5454,
- help="port to connect to on 'host' (defaults to 5454)")
-parser.add_argument("-c", "--nocolor", action="store_false", dest="color",
- help="discard minecraft chat control sequences "
- "instead of converting them directly to "
- "ansi control sequences")
-
-def convert (chat, toansi=True, cleanending=False) :
- if toansi:
- # if specified, add a "reset" minecraft control character, which
- # turns off underlines, italics, colors, and blinking.
- # (this could be used to prevent the "styling" in one command from
- # leaking into another or even the rest of the shell)
- if cleanending:
- chat += "§r"
-
- repl = lambda m: mapping[m.group(1)]
- else:
- repl = ""
-
- return ctlseq.sub(repl, chat)
-
-if __name__ == "__main__":
- args = parser.parse_args();
-
- if args.process:
- for line in sys.stdin:
- sys.stdout.write(convert(line, args.color))
-
- if args.color:
- # clean up after all the random control characters we just dumped to stdout
- sys.stdout.write(convert("", cleanending=True))
- else :
- interactive = sys.stdin.isatty() and sys.stdout.isatty()
-
- if interactive:
- # integrate readline features into input() if we can
- try:
- import readline
- except ImportError :
- pass
-
- import socket
-
- server = socket.create_connection((args.host, args.port))
-
- while True:
- if interactive:
- try:
- line = input("> ")+"\n"
- except EOFError:
- line = ""
- else:
- line = sys.stdin.readline()
-
- if not line:
- # clear out the prompt
- if interactive:
- print()
- break
- elif line.strip():
- # send the command as a utf-8-encoded string
- server.send(line.encode("utf-8"))
-
- # number of bytes left to read (taken from the response header,
- # defaults to unknown because we don't know yet)
- remaining = None
- # raw contents of the response header containing the length of
- # the body
- head = bytes()
- body = bytes()
-
- # read reply from the socket
- while True:
- # read a maximum of one kibibyte from the socket
- resp = server.recv(1024)
-
- if remaining is None:
- try:
- endofhead = resp.index(b"\n")+1
- except ValueError:
- head += resp
- continue
-
- head += resp[:endofhead]
- remaining = int(head.decode("utf-8"))
-
- # if the length specified in the header was 0, stop
- # because there's nothing else to read from the socket
- if not remaining:
- break
- # otherwise, chop off the
- else :
- resp = resp[endofhead:]
-
- body += resp
- remaining -= len(resp)
-
- # if there's no more to read, stop
- if remaining <= 0:
- break
-
- sys.stdout.write(convert(body.decode("utf-8"), args.color, True))
-
- server.close()
-
-
-
diff --git a/plugin/build.sh b/plugin/build.sh
deleted file mode 100755
index c0d4ed8..0000000
--- a/plugin/build.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-# warning: this ugly hack may cause aneurysms
-
-[ -z "$CLASSPATH" ] && {
- echo "no \$CLASSPATH specified. try:" >&2
- echo " CLASSPATH=/path/to/bukkit.jar $0" >&2
- exit 1
-}
-
-rm -rvf build
-cp -rv src build
-
-# remove vim swap files
-find build -name '.*.sw?' -delete
-
-pushd build/
- pushd io/github/ausbin/mccmd
- javac -Xlint:all *.java || exit 1
- rm *.java
- popd
-
- zip -r ../mccmd.jar ../../LICENSE *
-popd
-
diff --git a/plugin/src/config.yml b/plugin/config.yml
index 8afa340..8afa340 100644
--- a/plugin/src/config.yml
+++ b/plugin/config.yml
diff --git a/plugin/src/io/github/ausbin/mccmd/Config.java b/plugin/io/github/ausbin/mccmd/Config.java
index 84a57f2..6be2af9 100644
--- a/plugin/src/io/github/ausbin/mccmd/Config.java
+++ b/plugin/io/github/ausbin/mccmd/Config.java
@@ -29,7 +29,7 @@ import java.net.UnknownHostException;
import org.bukkit.configuration.file.FileConfiguration;
-public class Config {
+class Config {
public int port;
public boolean op;
public InetAddress addr;
diff --git a/plugin/io/github/ausbin/mccmd/ConnectionThread.java b/plugin/io/github/ausbin/mccmd/ConnectionThread.java
new file mode 100644
index 0000000..1e5be5d
--- /dev/null
+++ b/plugin/io/github/ausbin/mccmd/ConnectionThread.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2013 Austin Adams
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package io.github.ausbin.mccmd;
+
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+
+import java.nio.charset.Charset;
+
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.InetSocketAddress;
+
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+
+class ConnectionThread extends Thread {
+ Config config;
+ Socket client;
+ String remote;
+ Plugin parentPlugin;
+ Charset encoding;
+
+ public ConnectionThread (Socket client, Plugin parentPlugin, Config config) {
+ super("mccmd connection handler");
+
+ this.client = client;
+ // find a pretty string to associate with the client (host:port)
+ this.remote = this.remoteRepresentation(this.client.getRemoteSocketAddress());
+ this.config = config;
+ this.parentPlugin = parentPlugin;
+
+ try {
+ this.encoding = Charset.forName("UTF-8");
+ } catch (IllegalArgumentException e) {
+ this.parentPlugin.getLogger().severe("couldn't register utf-8 as a charset. get ready for some fun.");
+ e.printStackTrace();
+ }
+ }
+
+ private List<String> exec (String command) {
+ List<String> replies;
+
+ // create a throwaway object that will collect replies to the command
+ MessageReceiver receiver = new MessageReceiver(this.config.op, this.remote);
+
+ // give the permissions stated in the config file (if any)
+ for (Map.Entry<String,Boolean> perm : this.config.permissions.entrySet()) {
+ receiver.addAttachment(this.parentPlugin, perm.getKey(), perm.getValue());
+ }
+
+ this.parentPlugin.getLogger().info("client " + remote + " executed server command: " + command);
+ Bukkit.getServer().dispatchCommand(receiver, command);
+ replies = receiver.getMessages();
+
+ // throw away the object
+ receiver.stopAcceptingMessages();
+ receiver = null;
+
+ return replies;
+ }
+
+ // create a string representation of the remote client
+ private String remoteRepresentation (SocketAddress ip) {
+ if (ip == null || !(ip instanceof InetSocketAddress))
+ return "?";
+ else
+ return ((InetSocketAddress)ip).getHostString() + ":" + ((InetSocketAddress)ip).getPort();
+ }
+
+ // converts a string to a utf-8 encoded bytearray
+ private byte[] toBytes (String victim) {
+ return victim.getBytes(this.encoding);
+ }
+
+ private byte[] formResponseBody (List<String> replies) {
+ StringBuilder builder = new StringBuilder();
+
+ for (String reply : replies) {
+ builder.append(reply);
+ builder.append("\n");
+ }
+
+ return this.toBytes(builder.toString());
+ }
+
+ public void run () {
+ try {
+ // XXX stop being lazy and implement a this.fromBytes() and use a
+ // standalone InputStream
+ BufferedReader input = new BufferedReader(new InputStreamReader(this.client.getInputStream(), this.encoding));
+ OutputStream output = this.client.getOutputStream();
+
+ String command;
+
+ while ((command = input.readLine()) != null) {
+ List<String> replies = this.exec(command);
+ byte[] body = this.formResponseBody(replies);
+ byte[] header = this.toBytes(Integer.toString(body.length)+"\n");
+
+ output.write(header);
+ output.write(body);
+
+ output.flush();
+ }
+ this.client.close();
+ } catch (IOException e) {
+ if (this.client.isClosed()) {
+ return;
+ } else {
+ this.parentPlugin.getLogger().warning("communication with client failed!");
+ // XXX same concern as above
+ e.printStackTrace();
+
+ try {
+ this.client.close();
+ } catch (IOException f) {
+ this.parentPlugin.getLogger().warning("failed to close the client connection");
+ // XXX same concern as above
+ f.printStackTrace();
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/src/io/github/ausbin/mccmd/Mccmd.java b/plugin/io/github/ausbin/mccmd/Mccmd.java
index 2959628..99bcd1f 100644
--- a/plugin/src/io/github/ausbin/mccmd/Mccmd.java
+++ b/plugin/io/github/ausbin/mccmd/Mccmd.java
@@ -22,7 +22,6 @@
package io.github.ausbin.mccmd;
-import java.net.InetAddress;
import java.io.IOException;
import java.util.List;
import org.bukkit.plugin.java.JavaPlugin;
diff --git a/plugin/src/io/github/ausbin/mccmd/MessageReceiver.java b/plugin/io/github/ausbin/mccmd/MessageReceiver.java
index 4af83da..d7845d9 100644
--- a/plugin/src/io/github/ausbin/mccmd/MessageReceiver.java
+++ b/plugin/io/github/ausbin/mccmd/MessageReceiver.java
@@ -37,14 +37,17 @@ import org.bukkit.permissions.PermissibleBase;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo;
-public class MessageReceiver implements CommandSender, Permissible, ServerOperator {
+class MessageReceiver implements CommandSender, Permissible, ServerOperator {
private boolean op;
+ // string representation of the remote host (e.g. "10.0.2.1:3242")
+ private String remote;
private List<String> messages;
private PermissibleBase perm;
private boolean acceptingMessages;
- public MessageReceiver (boolean op) {
+ public MessageReceiver (boolean op, String remote) {
this.op = op;
+ this.remote = remote;
this.messages = new ArrayList<String>();
this.perm = new PermissibleBase(this);
this.acceptingMessages = true;
@@ -81,7 +84,7 @@ public class MessageReceiver implements CommandSender, Permissible, ServerOperat
}
public String getName () {
- return "mccmd";
+ return "mccmd@" + this.remote;
}
public Server getServer () {
diff --git a/plugin/io/github/ausbin/mccmd/SocketListenerThread.java b/plugin/io/github/ausbin/mccmd/SocketListenerThread.java
new file mode 100644
index 0000000..d245ad0
--- /dev/null
+++ b/plugin/io/github/ausbin/mccmd/SocketListenerThread.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2013 Austin Adams
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package io.github.ausbin.mccmd;
+
+import java.io.IOException;
+
+import java.net.Socket;
+import java.net.ServerSocket;
+import java.net.SocketException;
+
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+
+class SocketListenerThread extends Thread {
+ private Config config;
+ private Plugin parentPlugin;
+ protected ServerSocket socket;
+ private List<ConnectionThread> handlers;
+
+ public SocketListenerThread (Plugin parentPlugin, Config config) {
+ super("mccmd socket listener thread");
+
+ this.parentPlugin = parentPlugin;
+ this.config = config;
+ this.handlers = new ArrayList<ConnectionThread>();
+ }
+
+ //@Override
+ public void run () {
+ try {
+ this.socket = new ServerSocket(this.config.port, 0, this.config.addr);
+ this.socket.setReuseAddress(true);
+ } catch (IOException e) {
+ this.parentPlugin.getLogger().severe("couldn't bind to port " + this.config.port + "! bailing out");
+
+ // XXX is there a better way to do this? i would use
+ // Logger.throwing(), but that sets the severity of the traceback
+ // to FINEST, which is too low for the vanilla Logger to display.
+ // (this code puts it under SEVERE somehow)
+ e.printStackTrace();
+ return;
+ }
+
+ while (true) {
+ Socket client;
+
+ try {
+ client = this.socket.accept();
+ } catch (IOException e) {
+ // if the main thread closed our socket, end the thread
+ if (this.socket.isClosed()) {
+ // kill each of the open connections
+ for (ConnectionThread handler : this.handlers) {
+ if (handler.isAlive()) {
+ try {
+ handler.client.close();
+ } catch (IOException f) {
+ this.parentPlugin.getLogger().warning("couldn't close a client connection while shutting down");
+ // XXX same concern as above
+ f.printStackTrace();
+ }
+ }
+ }
+ // end this thread
+ return;
+ // otherwise it was probably a random networking problem.
+ // choke out an error and move on the the next connection.
+ } else {
+ this.parentPlugin.getLogger().warning("accept() failed");
+ // XXX same concern as above
+ e.printStackTrace();
+ continue;
+ }
+ }
+
+ ConnectionThread handler = new ConnectionThread(client, this.parentPlugin, this.config);
+ this.handlers.add(handler);
+ handler.start();
+ }
+ }
+}
diff --git a/plugin/src/plugin.yml b/plugin/plugin.yml
index 37ef7b2..546ea67 100644
--- a/plugin/src/plugin.yml
+++ b/plugin/plugin.yml
@@ -1,3 +1,3 @@
name: mccmd
main: io.github.ausbin.mccmd.Mccmd
-version: 0.2
+version: 0.3
diff --git a/plugin/src/io/github/ausbin/mccmd/SocketListenerThread.java b/plugin/src/io/github/ausbin/mccmd/SocketListenerThread.java
deleted file mode 100644
index 1b019b2..0000000
--- a/plugin/src/io/github/ausbin/mccmd/SocketListenerThread.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (c) 2013 Austin Adams
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-package io.github.ausbin.mccmd;
-
-import java.io.IOException;
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-
-import java.nio.charset.Charset;
-
-import java.net.Socket;
-import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.net.SocketException;
-
-import java.util.Map;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.logging.Logger;
-
-import org.bukkit.Bukkit;
-import org.bukkit.plugin.Plugin;
-
-public class SocketListenerThread extends Thread {
- private Config config;
- private Plugin parentPlugin;
- protected ServerSocket socket;
- private List<ConnectionHandler> handlers;
-
- public SocketListenerThread (Plugin parentPlugin, Config config) {
- super("mccmd socket listener thread");
-
- this.parentPlugin = parentPlugin;
- this.config = config;
- this.handlers = new ArrayList<ConnectionHandler>();
- }
-
- //@Override
- public void run () {
- try {
- this.socket = new ServerSocket(this.config.port, 0, this.config.addr);
- this.socket.setReuseAddress(true);
- } catch (IOException e) {
- this.parentPlugin.getLogger().severe("couldn't bind to port " + this.config.port + "! bailing out");
-
- // XXX is there a better way to do this? i would use
- // Logger.throwing(), but that sets the severity of the traceback
- // to FINEST, which is too low for the vanilla Logger to display.
- // (this code puts it under SEVERE somehow)
- e.printStackTrace();
- return;
- }
-
- while (true) {
- Socket client;
-
- try {
- client = this.socket.accept();
- } catch (IOException e) {
- // if the main thread closed our socket, end the thread
- if (this.socket.isClosed()) {
- // kill each of the open connections
- for (ConnectionHandler handler : this.handlers) {
- if (handler.isAlive()) {
- try {
- handler.client.close();
- } catch (IOException f) {
- this.parentPlugin.getLogger().warning("couldn't close a client connection while shutting down");
- // XXX same concern as above
- f.printStackTrace();
- }
- }
- }
- // end this thread
- return;
- // otherwise it was probably a random networking problem.
- // choke out an error and move on the the next connection.
- } else {
- this.parentPlugin.getLogger().warning("accept() failed");
- // XXX same concern as above
- e.printStackTrace();
- continue;
- }
- }
-
- ConnectionHandler handler = new ConnectionHandler(client, this.parentPlugin, this.config);
- this.handlers.add(handler);
- handler.start();
- }
- }
-
- private class ConnectionHandler extends Thread {
- Config config;
- Socket client;
- Plugin parentPlugin;
- Charset encoding;
-
- public ConnectionHandler (Socket client, Plugin parentPlugin, Config config) {
- super("mccmd connection handler");
-
- this.client = client;
- this.config = config;
- this.parentPlugin = parentPlugin;
-
- try {
- this.encoding = Charset.forName("UTF-8");
- } catch (IllegalArgumentException e) {
- this.parentPlugin.getLogger().severe("couldn't register utf-8 as a charset. get ready for some fun.");
- e.printStackTrace();
- }
- }
-
- private List<String> exec (String command) {
- List<String> replies;
-
- // create a throwaway object that will collect replies to the command
- MessageReceiver receiver = new MessageReceiver(this.config.op);
-
- // give the permissions stated in the config file (if any)
- for (Map.Entry<String,Boolean> perm : this.config.permissions.entrySet()) {
- receiver.addAttachment(this.parentPlugin, perm.getKey(), perm.getValue());
- }
-
- Bukkit.getServer().dispatchCommand(receiver, command);
- replies = receiver.getMessages();
-
- // throw away the object
- receiver.stopAcceptingMessages();
- receiver = null;
-
- return replies;
- }
-
- // converts a string to a utf-8 encoded bytearray
- private byte[] toBytes (String victim) {
- return victim.getBytes(this.encoding);
- }
-
- private byte[] formResponseBody (List<String> replies) {
- StringBuilder builder = new StringBuilder();
-
- for (String reply : replies) {
- builder.append(reply);
- builder.append("\n");
- }
-
- return this.toBytes(builder.toString());
- }
-
- public void run () {
- try {
- // XXX stop being lazy and implement a this.fromBytes() and use a standalone InputStream
- BufferedReader input = new BufferedReader(new InputStreamReader(this.client.getInputStream(), this.encoding));
- OutputStream output = this.client.getOutputStream();
-
- String command;
-
- while ((command = input.readLine()) != null) {
- List<String> replies = this.exec(command);
- byte[] body = this.formResponseBody(replies);
- byte[] header = this.toBytes(Integer.toString(body.length)+"\n");
-
- output.write(header);
- output.write(body);
-
- output.flush();
- }
- this.client.close();
- } catch (IOException e) {
- if (this.client.isClosed()) {
- return;
- } else {
- this.parentPlugin.getLogger().warning("communication with client failed!");
- // XXX same concern as above
- e.printStackTrace();
-
- try {
- this.client.close();
- } catch (IOException f) {
- this.parentPlugin.getLogger().warning("failed to close the client connection");
- // XXX same concern as above
- f.printStackTrace();
- }
- }
- }
- }
- }
-}