From: hm Date: Sun, 20 Mar 2016 19:19:47 +0000 (+0100) Subject: Initial state X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=HEAD;p=jatty Initial state --- 617befa841a28dc4eee5de7179ed6e7bdefee385 diff --git a/.classpath b/.classpath new file mode 100755 index 0000000..d639a08 --- /dev/null +++ b/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.project b/.project new file mode 100755 index 0000000..180dd81 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + jatty + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100755 index 0000000..ace45ce --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..c9adce7 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,5 @@ +/de/ +/jatty.de.properties +/jatty.en.properties +/talk_schema.sql +/user_schema.sql diff --git a/resources/jatty.de.properties b/resources/jatty.de.properties new file mode 100644 index 0000000..b418a71 --- /dev/null +++ b/resources/jatty.de.properties @@ -0,0 +1,13 @@ +0=Gebrauch: +1=Startet einen Chat-Server, einen GUI-Chat-Client oder einen Kommandozeilen-Chat-Client. +2=Adresse des Chat-Servers, z.B. localhost oder +3=Voreinstellung: +4=Port des Servers. +5=Voreinstellung: +6=Beispiele: +7=Modus fehlt: +8=Server angehalten +9=Sprache der Oberfläche. Voreinstellung: Systemeinstellung +10=Willkommen bei +11=Willkommen %s +12=Es fehlen Benutzername und Passwort: z.B. \ No newline at end of file diff --git a/resources/jatty.en.properties b/resources/jatty.en.properties new file mode 100644 index 0000000..1fab3ac --- /dev/null +++ b/resources/jatty.en.properties @@ -0,0 +1,13 @@ +0=Usage: +1=Starts a chat server, a GUI chat client or a commandline chat client. +2=address of the chat server, e.g. localhost or +3=Default: +4=port of the server. +5=Default: +6=Examples: +7=missing mode: +8=server stopped +9=language of the GUI. Default: system setting +10=Welcome to +11=Welcome %s +12=missing username and password: e.g. \ No newline at end of file diff --git a/resources/talk_schema.sql b/resources/talk_schema.sql new file mode 100755 index 0000000..9ec5269 --- /dev/null +++ b/resources/talk_schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE talk ( + talkId int identity, + time timestamp, + user varchar(64), + message varchar(254) +); +insert into talk (time, user, message) values('2016-03-16 08:10:32', 'hm', 'Hi all'); \ No newline at end of file diff --git a/resources/user_schema.sql b/resources/user_schema.sql new file mode 100755 index 0000000..49eaab2 --- /dev/null +++ b/resources/user_schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE user ( + user varchar(64), + passwdHash varchar(256), + lastLogin TIMESTAMP, + UNIQUE (user) +); +insert into user (user, passwdHash, lastLogin) values('hm', '', '2016-03-16 08:24:33'); diff --git a/src/de/hamatoma/jatty/Client.java b/src/de/hamatoma/jatty/Client.java new file mode 100755 index 0000000..8092b6a --- /dev/null +++ b/src/de/hamatoma/jatty/Client.java @@ -0,0 +1,118 @@ +/** + * + */ +package de.hamatoma.jatty; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.LinkedList; +import java.util.List; + +/** + * A TCP chat client. + * + * @author hm + * + */ +class Client { + String address; + int port = 0; + Socket clientSocket = null; + BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in)); + DataOutputStream outToServer = null; + BufferedReader inFromServer = null; + Scrambler scrambler; + + /** + * Constructor. + * + * @param address + * the address of the server, e.g. "localhost" or "192.168.2.1" + * @param port + * the port of the server + */ + public Client(final String address, final int port, + final Scrambler scrambler) { + this.port = port; + this.address = address; + this.scrambler = scrambler; + try { + this.clientSocket = new Socket(address, port); + this.outToServer = new DataOutputStream(this.clientSocket.getOutputStream()); + this.inFromServer = new BufferedReader(new InputStreamReader(this.clientSocket.getInputStream())); + } catch (final UnknownHostException e) { + e.printStackTrace(); + this.clientSocket = null; + } catch (final IOException e) { + e.printStackTrace(); + this.clientSocket = null; + } + } + + /** + * Writes a message to the user interface. + * + * @param msg + * the message to write + */ + public void log(final String msg) { + System.out.println(msg); + } + + /** + * Sends a message and receives the answer. + * + * @param message + * the message to send + * @return the answer of the server a a string list + */ + public List sendAndReceive(String message) { + final List answer = new LinkedList(); + try { + final boolean isUpdate = message.startsWith("/UPDATE"); + if (!isUpdate) + log("> " + message); + message = this.scrambler.encode(message + "\n"); + this.outToServer.writeBytes(message); + while (this.inFromServer.ready()) { + String line = this.inFromServer.readLine(); + line = this.scrambler.decode(line); + if (isUpdate && answer.size() == 0 && line.length() > 0) + log("> " + message); + log("< " + line); + answer.add(line); + } + } catch (final IOException e) { + e.printStackTrace(); + } + return answer; + } + + /** + * Runs the client untils the server sends "/EXIT". + * + * Only used by the command line client. + */ + public void run() { + String message; + List answer = null; + try { + while (answer == null || answer.size() == 0 + || answer.get(answer.size() - 1).startsWith("/EXIT")) { + message = this.inFromUser.readLine(); + answer = sendAndReceive(message); + } + } catch (final IOException e) { + e.printStackTrace(); + } + try { + this.clientSocket.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/de/hamatoma/jatty/ClientGui.java b/src/de/hamatoma/jatty/ClientGui.java new file mode 100644 index 0000000..bf6875c --- /dev/null +++ b/src/de/hamatoma/jatty/ClientGui.java @@ -0,0 +1,220 @@ +package de.hamatoma.jatty; + +import java.awt.BorderLayout; +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.List; + +import javax.swing.DefaultListModel; +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTextArea; +import javax.swing.ListSelectionModel; +import javax.swing.Timer; + +/** + * A chat client with a Swing GUI. + * + * @author hm + * + */ +public class ClientGui extends Client implements KeyListener, ActionListener { + + private JFrame frame; + private JTextArea textAreaCommand; + private JTextArea textAreaStatusLine; + private JTextArea textAreaHeader; + private final DefaultListModel talkListModel = new DefaultListModel(); + + /** + * A main program for testing. + * + * @param args + * program arguments + */ + public static void main(final String[] args) { + final String addr = args.length < 1 ? "localhost" : args[0]; + final int port = args.length < 2 ? 58123 : Integer.parseInt(args[1]); + final Scrambler scrambler = new Scrambler("a", "b", "c"); + final ClientGui client = new ClientGui(addr, port, scrambler); + client.run(); + } + /** + * Launch the application. + */ + @Override + public void run() { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + try { + ClientGui.this.frame.setVisible(true); + } catch (final Exception e) { + e.printStackTrace(); + } + } + }); + } + + Timer timer = null; + private JScrollPane scrollPaneTalk; + private JList listTalk; + private JScrollPane scrollPaneUser; + private JList listUser; + + /** + * Create the application. + * + * @param address + * the server address, e.g. "localhost" or "123.2.4.8" + * @param port + * the server port + */ + public ClientGui(final String address, final int port, + final Scrambler scrambler) { + super(address, port, scrambler); + initialize(); + this.timer = new Timer(1000, this); + this.timer.start(); + } + + /** + * Initialize the contents of the frame. + */ + private void initialize() { + this.frame = new JFrame(); + this.frame + .setTitle("jatty client [" + this.address + ":" + + this.port + "]"); + this.frame.setBounds(100, 100, 450, 300); + this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.frame.getContentPane().setLayout(new BorderLayout(0, 0)); + + final JPanel panel = new JPanel(); + this.frame.getContentPane().add(panel); + panel.setLayout(new BorderLayout(0, 0)); + + final JSplitPane splitPane = new JSplitPane(); + splitPane.setDividerLocation(0.80); + splitPane.setResizeWeight(0.9); + splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); + panel.add(splitPane, BorderLayout.CENTER); + + this.textAreaCommand = new JTextArea(); + this.textAreaCommand.setTabSize(3); + this.textAreaCommand.addKeyListener(this); + this.textAreaCommand.setText("/LOG "); + splitPane.setRightComponent(this.textAreaCommand); + + final JSplitPane splitPaneOutput = new JSplitPane(); + splitPaneOutput.setDividerLocation(0.80); + splitPaneOutput.setResizeWeight(0.8); + splitPane.setLeftComponent(splitPaneOutput); + + this.scrollPaneTalk = new JScrollPane(); + splitPaneOutput.setLeftComponent(this.scrollPaneTalk); + + this.listTalk = new JList(this.talkListModel); + this.listTalk.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + this.scrollPaneTalk.setViewportView(this.listTalk); + + this.scrollPaneUser = new JScrollPane(); + splitPaneOutput.setRightComponent(this.scrollPaneUser); + + this.listUser = new JList(); + this.listUser.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + this.scrollPaneUser.setViewportView(this.listUser); + + this.textAreaStatusLine = new JTextArea(); + this.textAreaStatusLine.setEditable(false); + this.textAreaStatusLine.setText("Started"); + this.frame.getContentPane().add(this.textAreaStatusLine, BorderLayout.SOUTH); + + this.textAreaHeader = new JTextArea(); + this.textAreaHeader.setEditable(false); + this.textAreaHeader.setText( + I18N.tr(I18N.WELCOME, "Welcome to") + " jatty " + Jatty.VERSION); + this.frame.getContentPane().add(this.textAreaHeader, BorderLayout.NORTH); + } + + /* + * (non-Javadoc) + * + * @see java.awt.event.KeyListener#keyTyped(java.awt.event.KeyEvent) + */ + @Override + public void keyTyped(final KeyEvent e) { + } + + /* + * (non-Javadoc) + * + * @see java.awt.event.KeyListener#keyPressed(java.awt.event.KeyEvent) + */ + @Override + public void keyPressed(final KeyEvent e) { + } + + /* + * (non-Javadoc) + * + * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent) + */ + @Override + public void keyReleased(final KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (e.getSource() == this.textAreaCommand){ + final String text = this.textAreaCommand.getText(); + final List answer = this.sendAndReceive(text); + handleAnswer(answer); + this.textAreaCommand.setText(""); + } + } + } + + /** + * Evaluates the server answer. + * + * @param answer + * the answer string list + */ + void handleAnswer(final List answer) { + for (final String line : answer) { + if (line.startsWith("/USER")) { + final String[] users = line.substring(6).split(" "); + this.listUser.setListData(users); + } else if (line.startsWith("/STATUS")) { + this.textAreaStatusLine.setText(line.substring(7)); + } else if (line.startsWith("/ERROR")) { + this.textAreaStatusLine.setText("+++ " + line.substring(7)); + } else { + this.talkListModel.addElement(line); + this.listTalk + .ensureIndexIsVisible(this.talkListModel.getSize() - 1); + } + } + + } + + /* + * (non-Javadoc) + * + * @see + * java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) + */ + @Override + public void actionPerformed(final ActionEvent e) { + if (e.getSource() == this.timer) { + final List answer = this.sendAndReceive("/UPDATE"); + if (!answer.isEmpty()) { + handleAnswer(answer); + } + } + } +} diff --git a/src/de/hamatoma/jatty/I18N.java b/src/de/hamatoma/jatty/I18N.java new file mode 100644 index 0000000..77fdebf --- /dev/null +++ b/src/de/hamatoma/jatty/I18N.java @@ -0,0 +1,107 @@ +/** + * + */ +package de.hamatoma.jatty; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * Internationalization: translations for the UI texts. + * + * @author hm + * + */ +public class I18N { + static public final int USAGE = 0; + static public final int USAGE2 = 1; + static public final int USAGE3 = 2; + static public final int USAGE4 = 3; + static public final int USAGE5 = 4; + static public final int USAGE6 = 5; + static public final int USAGE7 = 6; + static public final int MAIN1 = 7; + static public final int SERVER1 = 8; + static public final int USAGE8 = 9; + static public final int WELCOME = 10; + static public final int WELCOME2 = 11; + static public final int USERNAME = 12; + + static I18N classLoader = new I18N(); + static List translations = new ArrayList(); + + /** + * Reads the translations from a resource. + * + * @param prefix + * prefix of the file with relative path. Will be completed by + * the locale and ".properties" + * @param locale + * the locale shortcut, e.g. "de" or "en-us" + * @return true: the resource has been read + */ + public static boolean switchLanguage(final String prefix, + final String locale) { + boolean rc = false; + final String fn = "/" + prefix + "." + locale + ".properties"; + BufferedReader reader = null; + try { + final InputStream input = classLoader.getClass() + .getResourceAsStream(fn); + if (input != null) { + reader = new BufferedReader(new InputStreamReader(input, "UTF-8")); + if (reader != null) { + translations.clear(); + String line; + try { + while ((line = reader.readLine()) != null) { + if (line.matches("^\\d+=.*")) { + translations + .add(line.substring(line.indexOf('=') + 1)); + } + } + rc = true; + System.out + .println("successfully loaded: " + fn.substring(1)); + } catch (final IOException e) { + e.printStackTrace(); + } + } + } + } catch (final UnsupportedEncodingException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + } + } + return rc; + } + + /** + * Translates a message. + * + * @param index + * index of the message + * @param message + * default value of the message + * @return the translated message or - if not found - the default message + */ + public static String tr(final int index, final String message) { + final String rc; + if (index < translations.size()) + rc = translations.get(index); + else + rc = message; + return rc; + } +} diff --git a/src/de/hamatoma/jatty/Jatty.java b/src/de/hamatoma/jatty/Jatty.java new file mode 100755 index 0000000..7ff45ae --- /dev/null +++ b/src/de/hamatoma/jatty/Jatty.java @@ -0,0 +1,146 @@ +/** + * + */ +package de.hamatoma.jatty; + +import java.util.Locale; + +/** + * Starts a chat server or a chat client depending on the program arguments. + * + * @author wk + * + */ +public class Jatty { + public static final String VERSION = "v1.0.0 (2016.03.23)"; + + /** + * Extract the address from an argument. + * + * @param arg + * the argument. Syntax:
[:]
+ * If null "localhost" is returned + * @return the address of the IP expression + */ + static String getAddr(final String arg) { + String rc = arg; + final int pos = arg == null ? -1 : arg.indexOf(':'); + if (pos >= 0) + rc = arg.substring(0, pos); + return rc; + } + + /** + * Extract the port from an argument. + * + * @param arg + * the argument. Syntax:
[:]
+ * If null "localhost" is returned + * @return the address of the IP expression + */ + static int getPort(final String arg) { + int port = 58123; + final int pos = arg == null ? -1 : arg.indexOf(':'); + if (pos >= 0) + port = Integer.parseInt(arg.substring(pos + 1)); + return port; + } + + /** + * Writes a message to the output media. + * + * @param message message to write + */ + public static void log(final String message) { + System.out.println(message); + } + + /** + * Main program. + * + * Evaluates the program arguments and start a server or a client. + * + * @param args + * the program arguments + */ + public static void main(final String[] args) { + if (args.length == 0) { + usage(I18N.tr(I18N.MAIN1, "missing mode:") + + " server | client | clientcl"); + } else { + int pos0 = 0; + String arg0 = args[pos0]; + String locale = Locale.getDefault().getLanguage(); + I18N.switchLanguage("jatty", locale); + while (arg0 != null && arg0.startsWith("-")) { + if (arg0.startsWith("--locale=")) { + locale = arg0.substring(9); + if (!I18N.switchLanguage("jatty", locale)) { + log("+++ not supported locale " + locale + + ". Try --locale=de"); + } + } else { + usage("unknown option: " + arg0); + } + pos0++; + arg0 = args.length < pos0 ? args[pos0] : null; + } + if (arg0 == null) { + usage(I18N.tr(I18N.MAIN1, "missing mode:") + + " server | client | clientcl"); + } else { + final Scrambler scrambler = new Scrambler("Modern Times", + "Juwayakani", "2*5^8/3%"); + final String arg1 = pos0 + 1 >= args.length ? null + : args[pos0 + 1]; + if (arg0.equals("client")) { + final Client client = new ClientGui(getAddr(arg1), + getPort(arg1), scrambler); + client.run(); + } else if (arg0.equals("clientcl")) { + final Client client = new Client(getAddr(arg1), + getPort(arg1), scrambler); + client.run(); + } else if (arg0.equals("server")) { + final int port = arg1 == null ? getPort(null) + : Integer.parseInt(arg1); + final Server server = new Server(port, scrambler); + server.run(); + } else { + usage("unknown mode: " + arg0); + } + } + } + } + + /** + * Prints a "usage" message. + * + * @param message + * null or the error message + */ + public static void usage(final String message) { + System.out.println(I18N.tr(I18N.USAGE, "Usage:") + + " jatty { server | { client | clientcl } [[:]] }\n" + + I18N.tr(I18N.USAGE2, + "Starts a chat server, a GUI chat client or a commandline chat client.") + + "\n" + "\n\t" + + I18N.tr(I18N.USAGE3, + "address of the chat server, e.g. localhost or") + + " 192.168.3.4\n" + " " + I18N.tr(I18N.USAGE4, "Default:") + + " localhost\n" + "\n\t" + + I18N.tr(I18N.USAGE5, "port of the server.") + "\n\t" + + I18N.tr(I18N.USAGE6, "Default:") + "58123\n" + + ":\n--locale=\n\t" + + I18N.tr(I18N.USAGE8, + "language of the GUI. Default: system setting") + + I18N.tr(I18N.USAGE7, "Examples:") + "\n" + "jatty server\n" + + "jatty --locale=de server 8888\n" + + "jatty client localhost:8888\n" + + "jatty --locale=de client 123.4.3.55\n" + + "jatty clientcl 194.168.22.4:8888"); + if (message != null) + System.out.println("+++ " + message); + } + +} diff --git a/src/de/hamatoma/jatty/JattyDb.java b/src/de/hamatoma/jatty/JattyDb.java new file mode 100755 index 0000000..553c5ef --- /dev/null +++ b/src/de/hamatoma/jatty/JattyDb.java @@ -0,0 +1,17 @@ +/** + * + */ +package de.hamatoma.jatty; + +/** + * @author wk + * + */ +public class JattyDb { + public JattyDb(){ + + } + public void initTables(){ + + } +} diff --git a/src/de/hamatoma/jatty/Scrambler.java b/src/de/hamatoma/jatty/Scrambler.java new file mode 100644 index 0000000..6025e92 --- /dev/null +++ b/src/de/hamatoma/jatty/Scrambler.java @@ -0,0 +1,261 @@ +/** + * + */ +package de.hamatoma.jatty; + +/** + * + * Encoding/decoding of strings. + * + * @author hm + * + */ +public class Scrambler implements Cloneable { + static long[] primes = { // + 4794328877860651L, // 11086aadb3872b + 4958110297415603L, // 119d600308dfb3 + 4623102848326049L, // 106cb00308d9a1 + 4891108807597633L, // 1160700308de41 + 4578435188447437L, // 1044100308d8cd + 5546234485817489L, // 13b445585e4091 + 5389897676242243L, // 132615585e3d43 + 4786884267880873L, // 1101a5585e31a9 + 5241005476646707L, // 129eaaadb38f33 + 4734771998022511L, // 10d2400308db6f + 5278228526545621L, // 12c085585e3ad5 + 4652881288245079L, // 1087c5585e2f57 + 5181448596808493L, // 1268800308e32d + 4920887247516739L, // 117b85585e3443 + 5360119236323197L, // 130b000308e77d + 5471788386019861L, // 1370900308ea15 + 4645436678265319L, // 1081000308d9e7 + 4481655258710579L, // fec0aadb38233 + }; + public static void log(final String text) { + System.out.println(text); + } + public static void main(final String[] args) { + final Scrambler scrambler = new Scrambler("a", "b", "c"); + test("What a wonderful world!", scrambler); + test( + ",.-;:_^\\!\"§$%&/()=?<>|ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890abcdefghijklmnopqrstuvwxyz 1234567890ßöäüÖÄÜ", + scrambler); + String text = ""; + for (int ix = 0; ix < 5; ix++) { + text = "xxx" + (char) ('a' + ix); + log(String.format("%s: %x", text, scrambler.hash(text))); + } + text = ""; + for (int ix = 0; ix < 5; ix++) { + text = text + "a"; + log(String.format("%6s: %x", text, scrambler.hash(text))); + } + for (int ix = 0; ix < 5; ix++) { + text = "" + (char) ('a' + ix); + log(String.format("%s: %x", text, scrambler.hash(text))); + } + for (int ix = 0; ix < 5; ix++) { + text = (char) ('a' + ix) + "xxx"; + log(String.format("%s: %x", text, scrambler.hash(text))); + } + scrambler.setSeed(0); + for (int ix = 0; ix < 5; ix++) { + log(String.format("%d: %x", ix, scrambler.nextLong())); + } + log(""); + } + private static void test(final String text, final Scrambler scrambler) { + final String text2 = scrambler.encode(text); + final String text3 = scrambler.decode(text2); + if (!text.equals(text3)) + log("not equal:\n" + text + "\n" + text3); + + } + int callNumber = 0; + long factor = 7; + long mask = 0xabcd2233L; + long offset = 0x123321293456545L; + long savedSeed; + long seed = 0x19580312; + long trueRandomSeed = System.currentTimeMillis() + ^ (primes[0]); + + /** + * Constructor. + * + * @param password1 + * first pass phrase + * @param password2 + * second pass phrase + * @param password3 + * 3rd pass phrase + */ + public Scrambler(final String password1, final String password2, + final String password3) { + if ((this.factor = hash(password1)) == 0) + this.factor = 0x17023947a2b3e477L; + this.offset = hash(password2); + this.mask = hash(password3); + // use an unitialized value: + final long random = Runtime.getRuntime().freeMemory(); + this.trueRandomSeed += primes[(int) (random % 0xffffffffL) + % primes.length] ^ Runtime.getRuntime().maxMemory(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#clone() + */ + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + /** + * Decodes a string encoded by encode(). + * + * @param text + * encoded text + * @return the clear text + */ + public String decode(final String text) { + final StringBuilder rc = new StringBuilder(); + setSeed(text.charAt(0), text.charAt(1), text.charAt(2), text.charAt(3)); + for (int ix = 4; ix < text.length(); ix++){ + int cc = text.charAt(ix); + if (cc >= 32) { + final int offset = nextInt(0, 256); + cc = cc - 32 - offset; + if (cc < 0) + cc += (0x1000 + 32); + cc += 32; + } + rc.append((char) cc); + } + return rc.toString(); + } + + /** + * Encodes a string. + * + * @param text + * clear text + * @return the encoded text + */ + public String encode(final String text) { + final StringBuilder rc = new StringBuilder(); + // salt: 4 char: + switchToTrueRandom(true); + rc.append((char) nextInt(32, 256)).append((char) nextInt(32, 256)); + rc.append((char) nextInt(32, 256)).append((char) nextInt(32, 256)); + switchToTrueRandom(false); + setSeed(rc.charAt(0), rc.charAt(1), rc.charAt(2), rc.charAt(3)); + for (int ix = 0; ix < text.length(); ix++){ + int cc = text.charAt(ix); + if (cc >= 32) { + final int offset = nextInt(0, 256); + cc = 32 + ((cc - 32 + offset) % (0x10000 - 32)); + } + rc.append((char) cc); + } + return rc.toString(); + } + + /** + * Converts a string into a long value. + * + * @param text + * text to convert + * @return a long value + */ + public long hash(final String text) { + long rc = primes[0]; + int ixPrimes = primes.length - 1; + for (int ix = text.length() - 1; ix >= 0; ix--) { + rc = (((rc << 1) | (rc >> 63)) + primes[ixPrimes] * text.charAt(ix) + + primes[ixPrimes - 1] + + primes[text.charAt(ix == 0 ? 0 : ix - 1) % primes.length]) + ^ (primes[(text.length() - ix) % primes.length]); + if ((ixPrimes -= 2) <= 0) + ixPrimes = primes.length - 1; + } + return rc; + } + + /** + * Returns a pseudo random value in a given range. + * + * @param min + * the minimum value (inclusive) + * @param max + * the maximum value (exclusive) + * @return a pseudo random value x with min <= x < max + */ + public int nextInt(final int min, final int max) { + final int rc = min + (int) ((nextLong() >> 12) % (max - min + 1)); + return rc; + + } + + /** + * Returns the next pseudo random value. + * + * @return a pseudo random value as long value (always not negative) + */ + public long nextLong() { + this.seed = this.seed * this.factor + this.offset; + this.seed = (this.seed << 41) | (this.seed >> (64 - 41)); + final long rc = ((this.seed ^ this.mask) + ^ primes[++this.callNumber % primes.length]) & 0x7fffffffffffffL; + return rc; + } + + /** + * Resets the pseudo random generator. + * + * @param seed1 + * first seed value + * @param seed2 + * 2nd seed value + * @param seed3 + * 3rd seed value + * @param seed4 + * 4th seed value + */ + public void setSeed(final int seed1, final int seed2, final int seed3, + final int seed4) { + this.callNumber = 0; + setSeed((((long) seed1) << 56) | (((long) seed2) << 48) + | (((long) seed1) << 40) | (((long) seed1) << 32) + | (((long) seed1 + seed2) << 24) | (((long) seed2 + seed3) << 16) + | (((long) seed3 + seed4) << 8) + (seed1 + seed2 + seed3 + seed4)); + } + + /** + * Resets the pseudo random generator. + */ + public void setSeed(final long seed) { + this.seed = primes[8] ^ seed; + } + + /** + * Switch random generator from pseudo to true or vice versa. + * + * @param nowTrueRandom + * true: the random generator will be return "true" + * random now + */ + private void switchToTrueRandom(final boolean nowTrueRandom) { + if (nowTrueRandom) { + this.savedSeed = this.seed; + this.trueRandomSeed = (this.trueRandomSeed << 17) + + System.currentTimeMillis() + + (this.trueRandomSeed >> (63 - 17)); + this.seed = this.trueRandomSeed; + } else { + this.seed = this.savedSeed; + } + + } +} diff --git a/src/de/hamatoma/jatty/Server.java b/src/de/hamatoma/jatty/Server.java new file mode 100755 index 0000000..b788d70 --- /dev/null +++ b/src/de/hamatoma/jatty/Server.java @@ -0,0 +1,369 @@ +/** + * + */ +package de.hamatoma.jatty; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +/** + * Stores the properties of a connection for one client. + * + * @author hm + * + */ +class Connection extends Thread { + Socket connectionSocket; + Scrambler scrambler; + Server server; + UpdateStatus updateStatus = new UpdateStatus(); + String user; + + public Connection(final Socket connectionSocket, final Server server) { + this.connectionSocket = connectionSocket; + this.user = this.connectionSocket.getInetAddress().getHostAddress(); + this.scrambler = server.cloneScrambler(); + log("new connection: " + this.user); + this.server = server; + start(); + } + + /** + * @return the user name of the connection + */ + public String getUser() { + return this.user; + } + + public void log(final String message) { + System.out.println(message); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Thread#run() + */ + @Override + public void run() { + boolean again = true; + while (again) { + try { + final BufferedReader inFromClient = new BufferedReader( + new InputStreamReader( + this.connectionSocket.getInputStream())); + final DataOutputStream outToClient = new DataOutputStream( + this.connectionSocket.getOutputStream()); + try { + String clientSentence = inFromClient.readLine(); + clientSentence = this.scrambler.decode(clientSentence); + String answer = null; + if (clientSentence != null) { + if (!clientSentence.startsWith("/UPDATE")) + log("> " + clientSentence); + if (clientSentence.startsWith("/EXIT")) { + again = false; + } else if (clientSentence.startsWith("/LOG")) { + final String[] args = clientSentence.split("\\s+"); + if (args.length > 1) { + + this.user = this.server.findUnusedUser(args[1]); + this.server.setUpdateNoUser(); + answer = this.server.getUpdate(this.updateStatus, + "/STATUS " + String.format( + I18N.tr(I18N.WELCOME2, "welcome"), + this.user)); + } else { + answer = "/ERROR " + + I18N.tr(I18N.USERNAME, + "missing username and password: e.g.") + + " /LOG jonny s!ekret\n"; + } + } else if (clientSentence.startsWith("/UPDATE")) { + answer = this.server.getUpdate(this.updateStatus, null); + } else { + final Calendar cal = Calendar.getInstance(); + final String line = String.format( + "%02d:%02d:%02d %s: %s", + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND), + this.user, clientSentence); + this.server.putTalkLine(line); + answer = this.server.getUpdate(this.updateStatus, null); + log("< " + answer.substring(0, answer.length())); + } + if (answer != null) + outToClient.writeBytes(answer); + } + } catch (final SocketException e2) { + log("+++ connection lost"); + again = false; + } + } catch (final IOException e) { + e.printStackTrace(); + again = false; + } + } + } +} + +/** + * A chat server. + * + * @author wk + * + */ +public class Server { + List connections = new ArrayList(); + int maxTalkLines = 1024; + int port = 58123; + Scrambler scrambler; + List talkLines = new ArrayList(); + int talkLinesOffset = 0; + /// stores the update number when the last login has been done + int updateNoUser = 0; + + /** + * Constructor. + * + * @param port + * the port listened by the server + */ + public Server(final int port, final Scrambler scrambler) { + this.scrambler = scrambler; + this.port = port; + } + + /** + * Search a name which is not used by another connection + * + * @param user + * the wanted user name + * @return user if this name is not used
+ * otherwise: a modification of the username + */ + public String findUnusedUser(final String user) { + String rc = user; + if (userExists(user)) { + int no = 1; + while (userExists(rc = user + no)) + no++; + } + return rc; + } + + /** + * @return a clone of the the scrambler + */ + public Scrambler cloneScrambler() { + Scrambler rc = null; + try { + rc = (Scrambler) this.scrambler.clone(); + } catch (final CloneNotSupportedException e) { + e.printStackTrace(); + } + return rc; + } + + /** + * Returns the data not yet reported to the client. + * + * @param updateStatus + * IN/OUT: the "ages" of the resources + * @param message + * null or an additional message to send + * @return the lines not yet sent as a string + */ + public String getUpdate(final UpdateStatus updateStatus, + final String message) { + final StringBuilder buffer = new StringBuilder(); + synchronized (this) { + final int count = this.talkLines.size(); + final int start = updateStatus.getMessageNo() + - this.talkLinesOffset; + if (start < count) { + for (int ix = start; ix < count; ix++) { + buffer.append(this.scrambler.encode(this.talkLines.get(ix))) + .append('\n'); + } + updateStatus.setMessageNo(this.talkLinesOffset + count); + } + if (updateStatus.getUserNo() < this.updateNoUser) { + final StringBuilder buffer2 = new StringBuilder(); + buffer2.append("/USER"); + for (final Connection connection : this.connections) { + buffer2.append(' ').append(connection.getUser()); + } + buffer2.append('\n'); + buffer.append(this.scrambler.encode(buffer2.toString())); + updateStatus.setUserNo(this.updateNoUser); + } + } + if (message != null) { + buffer.append(this.scrambler.encode(message)).append('\n'); + } + return buffer.toString(); + } + + /** + * Writes a message to stdout. + * + * @param message + * the message to write + */ + public void log(final String message) { + System.out.println(message); + } + + /** + * Collects the answer string list. + * + * Only the missing information will be returned. + * + * @param updateNo + * the "age" of the message list since the last query of this + * connection + * @param message + * null or an additional line of the answer string + * list + * @return + */ + public String prepareAnswer(final int updateNo, final String message) { + final StringBuilder buffer = new StringBuilder(); + synchronized (this) { + + } + return buffer.toString(); + } + + /** + * Stores a chat message into the message list. + * + * @param line + * the line to store + */ + public void putTalkLine(final String line) { + synchronized (this) { + if (this.talkLines.size() >= this.maxTalkLines) { + this.talkLines.remove(0); + this.talkLinesOffset++; + } + this.talkLines.add(line); + } + } + + /** + * Removes a connection from the connection list. + * + * @param connection + */ + public void removeConnection(final Connection connection) { + this.connections.remove(connection); + } + + /** + * Runs the chat server. + */ + public void run() { + ServerSocket welcomeSocket = null; + log("jatty " + Jatty.VERSION); + log("0.0.0.0:" + this.port); + try { + welcomeSocket = new ServerSocket(this.port); + while (true) { + final Socket socket = welcomeSocket.accept(); + final Connection connection = new Connection(socket, this); + this.connections.add(connection); + } + } catch (final Exception e) { + e.printStackTrace(); + } finally { + if (welcomeSocket != null) + try { + welcomeSocket.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + } + log(I18N.tr(I18N.SERVER1, "server stopped")); + } + + /** + * Increments the "age" of user list. + */ + public void setUpdateNoUser() { + synchronized (this) { + this.updateNoUser++; + } + } + + /** + * Tests whether a given user exists. + * + * @param user + * username to test + * @return true: a user with the given name exists + */ + public boolean userExists(final String user) { + boolean rc = false; + synchronized (this) { + for (final Connection connection : this.connections) { + if (connection.getUser().equals(user)) { + rc = true; + break; + } + } + } + return rc; + } +} + +/** + * A utility class for storing "ages" of the changeable resources. + * + * @author hm + * + */ +class UpdateStatus { + int messageNo = 0; + + int userNo = 0; + + /** + * @return the messageNo + */ + public int getMessageNo() { + return this.messageNo; + } + + /** + * @return the userNo + */ + public int getUserNo() { + return this.userNo; + } + + /** + * @param messageNo + * the messageNo to set + */ + public void setMessageNo(final int messageNo) { + this.messageNo = messageNo; + } + + /** + * @param userNo + * the userNo to set + */ + public void setUserNo(final int userNo) { + this.userNo = userNo; + } +} \ No newline at end of file