package net.pterodactylus.util.telnet; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import net.pterodactylus.util.io.Closer; import net.pterodactylus.util.logging.Logging; import net.pterodactylus.util.service.AbstractService; import net.pterodactylus.util.telnet.Command.Reply; import net.pterodactylus.util.text.StringEscaper; import net.pterodactylus.util.text.TextException; /** * Handles a single client connection. * * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a> */ public class ControlConnection extends AbstractService { /** The logger. */ private static final Logger logger = Logging.getLogger(ControlConnection.class.getName()); /** The line break. */ private static final String LINEFEED = "\r\n"; /** The client’s input stream. */ private final InputStream clientInputStream; /** The client’s output stream. */ private final OutputStream clientOutputStream; /** The output stream writer. */ private final PrintWriter outputStreamWriter; /** Mapping from command names to commands. */ Map<String, Command> commands = new HashMap<String, Command>(); /** Mapping from internal command names to commands. */ Map<String, Command> internalCommands = new HashMap<String, Command>(); /** * Creates a new connection handler for a client on the given socket. * * @param clientInputStream * The client input stream * @param clientOutputStream * The client output stream */ public ControlConnection(InputStream clientInputStream, OutputStream clientOutputStream) { this.clientInputStream = clientInputStream; this.clientOutputStream = clientOutputStream; this.outputStreamWriter = new PrintWriter(clientOutputStream); addCommand(new QuitCommand()); } // // ACCESSORS // /** * Adds the given command to this control. * * @param command * The command to add */ public void addCommand(Command command) { commands.put(command.getName().toLowerCase(), command); internalCommands.put("help", new HelpCommand(commands.values())); } // // ACTIONS // /** * Prints the given line to the output stream. * * @param line * The line to print */ public void addOutputLine(String line) { addOutputLines(line); } /** * Prints the given lines to the output stream. * * @param lines * The lines to print */ public void addOutputLines(String... lines) { synchronized (outputStreamWriter) { for (String line : lines) { outputStreamWriter.println(line); } outputStreamWriter.flush(); } } // // SERVICE METHODS // /** * {@inheritDoc} */ @Override protected void serviceRun() { InputStreamReader inputStreamReader = null; BufferedReader bufferedReader = null; try { inputStreamReader = new InputStreamReader(clientInputStream); bufferedReader = new BufferedReader(inputStreamReader); String line; boolean finished = false; while (!finished && ((line = bufferedReader.readLine()) != null)) { line = line.trim(); if (line.length() == 0) { continue; } List<String> words; try { words = StringEscaper.parseLine(line); } catch (TextException te1) { writeReply(new Reply(Reply.BAD_REQUEST).addLine("Syntax error.")); continue; } if (words.isEmpty()) { continue; } String commandName = words.remove(0).toLowerCase(); List<Command> foundCommands = findCommand(commandName); if (foundCommands.isEmpty()) { writeReply(new Reply(Reply.NOT_FOUND).addLine("Command not found.")); } else if (foundCommands.size() == 1) { Command command = foundCommands.get(0); try { Reply commandReply = command.execute(words); writeReply(commandReply); } catch (IOException ioe1) { throw ioe1; } catch (Throwable t1) { writeReply(new Reply(Reply.INTERNAL_SERVER_ERROR).addLine("Internal server error: " + t1.getMessage())); } if (command instanceof QuitCommand) { finished = true; } } else { Reply reply = new Reply(Reply.MULTIPLE_CHOICES, "Multiple choices found:"); for (Command command : foundCommands) { reply.addLine(command.getName()); } writeReply(reply); } } } catch (IOException ioe1) { logger.log(Level.INFO, "could not handle connection", ioe1); } finally { Closer.close(outputStreamWriter); Closer.close(bufferedReader); Closer.close(inputStreamReader); } } /** * {@inheritDoc} */ @Override protected void serviceStop() { Closer.close(clientInputStream); Closer.close(clientOutputStream); } // // PRIVATE METHODS // /** * Searches both internal and user commands for a command. A command must * have a name that equals or starts with the given name to be a match. * * @param name * The name of the command * @return All found commands */ private List<Command> findCommand(String name) { List<Command> foundCommands = new ArrayList<Command>(); for (Command command : internalCommands.values()) { if (command.getName().toLowerCase().startsWith(name.toLowerCase())) { foundCommands.add(command); } } for (Command command : commands.values()) { if (command.getName().toLowerCase().startsWith(name.toLowerCase())) { foundCommands.add(command); } } return foundCommands; } /** * Writes the given reply to the client’s output stream. The * <code>reply</code> may be <code>null</code> in which case an appropriate * error message is written. * * @param reply * The reply to send * @throws IOException * if an I/O error occurs */ private void writeReply(Reply reply) throws IOException { synchronized (outputStreamWriter) { if (reply == null) { outputStreamWriter.write("500 Internal server error." + LINEFEED); outputStreamWriter.flush(); return; } int status = reply.getStatus(); List<String> lines = reply.getLines(); for (int lineIndex = 0, lineCount = lines.size(); lineIndex < lineCount; lineIndex++) { outputStreamWriter.write(status + ((lineIndex < (lineCount - 1)) ? "-" : " ") + lines.get(lineIndex) + LINEFEED); } if (lines.size() == 0) { outputStreamWriter.write("200 OK." + LINEFEED); } outputStreamWriter.flush(); } } }