Samstag, 21. März 2015

C# Chat Client v1

Im heutigen Post möchte ich alle vorigen Posts verbinden um eine erste Version meines Chat Clients vorzustellen. Der tatsächliche Chat Client ist in C# geschrieben und sieht so aus:


















Es gibt einen zentralen Server, auf welchem PHP Skripte verfügbar sind. Diese dienen der Benutzerverwaltung (Registrierung / Login), und der Nachrichtenverwaltung. Alle Daten werden in einer MySQL Datenbank auf dem Server gespeichert. Es gibt also eine Tabelle mit den Benutzern sowie eine mit den Nachrichten. Beim Schicken einer Nachricht wird das entsprechende PHP Skript vom Client aufgerufen, dieses schreibt die Nachricht dann in die Datenbank hinein. Der Client des Empfängers ruft periodisch ein Skript zur Nachrichtenabholung auf, dieses leitet ihm dann eventuelle Nachrichten weiter.

Es gibt im wesentlichen 2 Klassen: Die Klasse Form1 zur Verwaltung der grafischen Oberfläche, sowie die Klasse SimpleChatClient, welche den Kern des Chat Clients darstellt und die Kommunikation mit dem Server erledigt. 
Das Design der Oberfläche ist das gleiche wie beim erweiterten Facebook Chat, ich möchte auf den Code der Klasse hier nicht eingehen, sondern mich auf den Kern des Chats konzentrieren.

In der Klasse SimpleChatClient ist die Funktion HTTPPost() von zentraler Bedeutung. Sie erzeugt HTTP POST Requests mit der HttpWebRequest Klasse und benutzt außerdem Cookies, sodass wir PHP Sessions benutzen können. Die gewünschte Adresse und die Parameter übergeben wir an die Funktion. Die Funktionen Register(), Login(), Send() und Receive() haben die zu erwartenden Funktionen und rufen HTTPPost() mit den passenden Argumenten auf. Die Funktionen sind sehr einfach, sie leiten im wesentlichen nur die Anfrage an die passenden PHP Skripte weiter und die (eventuellen) Ausgaben an die Chat Oberfläche. Die Funktionen Send() und Receive() sollen natürlich nur den jeweiligen Benutzern zur Verfügung stehen, deshalb prüfen die dazu gehörigen PHP Skripte, ob der aktuelle Benutzer bereits eine Session gestartet hat. Die globale Variable Cookie speichert den Cookie für den HTTP Request, welcher beim Einloggen zurückgegeben wird.

Kommen wir nun zum PHP Code auf dem Server. Alle PHP Skripte haben am Anfang die Codezeile include("connect.php");, welche das Skript connect.php einbindet. Dieses bereitet lediglich die MySQL Verbindung durch $conn = new mysqli(server, username, password, database); vor. Die Daten dazu werde ich hier nicht veröffentlichen, da ich möchte, dass der Client auch (sicher) benutzt werden kann.
Natürlich kann jeder seinen eigenen Server erstellen und muss dafür nur das Skript connect.php ändern (und natürlich die Adressen im C# Code), in der Datenbank werden die folgenden Tabellen benötigt: Users, mit den Attributen username und password, und Messages mit Sender, Recipient und Message.
Ich möchte der Einfachheit halber wieder zuerst PHP Code mit der Klasse mysql zeigen, im nächsten Post dann die sichere Version mit mysqli.
Die Dateien register.php und login.php sind die im Post über ein Loginsystem vorgestellten:

register.php (http://bloggeroliver.bplaced.net/Chat/Insecure/register.php):

<?php
include("connect.php");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$checkuser = mysql_query("SELECT username FROM Users WHERE username = '$username'");
$num_rows = mysql_num_rows($checkuser);

if ($num_rows > 0) {
     echo "Existing";
}
else {
     mysql_query("INSERT INTO Users (username, password) VALUES ('$username', '$hashedpw');");
     echo "Success";
}
?>

login.php (http://bloggeroliver.bplaced.net/Chat/Insecure/login.php):

<?php
session_start();

include("connect.php");

$username = $_POST["username"];
$password = $_POST["password"];
$hashedpw = md5($password);

$result = mysql_query("SELECT username, password FROM Users WHERE username = '$username' LIMIT 1");
$row = mysql_fetch_object($result);
    
if($row->password == $hashedpw) {
    $_SESSION["username"] = $username;
    echo "LoginGood";
}
else {
    echo "LoginBad";
}
?>

In der Datei send.php (http://bloggeroliver.bplaced.net/Chat/Insecure/send.php) wird zuerst auf eine gültige Session geprüft, und falls diese existiert wird die übergebene Nachricht in die Datenbank geschrieben, wobei der angemeldete Nutzer als Absender eingetragen wird:

<?php
session_start();

include("connect.php");

$Recipient = $_POST["Recipient"];
$Message = $_POST["Message"];
$Sender = $_SESSION['username'];

if(!isset($_SESSION['username'])) {
     echo "Login first.";
     exit;
}

mysql_query("INSERT INTO Messages () VALUES ('$Sender', '$Recipient', '$Message');");
?>

In receive.php (http://bloggeroliver.bplaced.net/Chat/Insecure/receive.php) wird ebenfalls zuerst auf eine gültige Session geprüft, und dann werden alle Nachrichten mit dem aktuellen Nutzer als Empfänger aus der Datenbank gelesen, zurückgegeben und anschließend gelöscht:

<?php

session_start();

include("connect.php");

if(!isset($_SESSION['username']))
   {
   echo "Bitte erst login";
   exit;
   }
    
$Recipient = $_SESSION['username'];

$eintrag = "SELECT Sender, Message FROM Messages WHERE Recipient = '$Recipient'";
$eintragen = mysql_query($eintrag);
while($row = mysql_fetch_object($eintragen))
   {
          echo "$row->Sender<br />";
          echo "$row->Message<br />";
   }
$delete  = "DELETE FROM Messages WHERE Recipient = '$Recipient'";
$eintragen2 = mysql_query($delete);
?>

Nun der Code des C# Clients:

Form1.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Net;
using System.IO;

namespace SimpleChatClient
{
    public class SimpleChatClient
    {
        public class User
        {
            public string Username;

            public User(string username)
            {
                Username = username;
            }
        }

        public class Message
        {
            public string Sender;
            public string Msg;

            public Message(string sender, string message)
            {
                Sender = sender;
                Msg = message;
            }
        }

        User CurrentUser = null;
        CookieContainer Cookie = null;
        string ServerUrl = "http://bloggeroliver.bplaced.net/Chat/Insecure/";

        public string GetUsername()
        {
            return CurrentUser.Username;
        }

        private string HTTPPost(string url, string postparams)
        {
            string responseString = "";

            try
            {
                // performs the desired http post request for the url and parameters
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.CookieContainer = Cookie; // explicitely use the cookiecontainer to save the session

                string postData = postparams;
                byte[] data = Encoding.UTF8.GetBytes(postData);

                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
                request.ContentLength = data.Length;

                using (var stream = request.GetRequestStream())
                {
                    stream.Write(data, 0, data.Length);
                }

                var response = (HttpWebResponse)request.GetResponse();

                responseString = new StreamReader(response.GetResponseStream()).ReadToEnd();

                return responseString;
            }
            catch
            {
                // MessageBox.Show("error" + url + postparams + responseString);
                return null;
            }
        }

        public string Register(string username, string password)
        {
            // register a new user
            return HTTPPost(ServerUrl + "register.php", "username=" + username + "&password=" + password);
        }

        public bool Login(string username, string password)
        {
            // login
            Cookie = new CookieContainer();
            string Login = HTTPPost(ServerUrl + "login.php", "username=" + username + "&password=" + password);
            if (Login == "LoginGood")
            {
                CurrentUser = new User(username);
                return true;
            }
            else
            {
                Cookie = null;
                return false;
            }
        }

        public void Logout()
        {
            // logout
            CurrentUser = null;
            Cookie = null;
        }

        public string Send(string recipient, string message)
        {
            // send
            return (HTTPPost(ServerUrl + "send.php", "Recipient=" + recipient + "&Message=" + message));
        }

        public List<Message> Receive()
        {
            // receive messages
            if (CurrentUser == null)
                return new List<Message>();
            string Messages = HTTPPost(ServerUrl + "receive.php", "");
            if (Messages == null)
                return new List<Message>();

            // message format is: sender<br />message<br />, split regarding to this in single messages
            string[] Splits = Messages.Split(new string[] { "<br />" }, StringSplitOptions.None);
            List<Message> Received = new List<Message>();
            for (int i = 0; i < Splits.Length - 1; i += 2)
            {
                Received.Add(new Message(Splits[i], Splits[i + 1]));
            }
            return Received;
        }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        SimpleChatClient Client;
        bool LoggedIn = false;
        Dictionary<string, Chat> Chats = new Dictionary<string, Chat>(); // stores all active chats

        private void button1_Click(object sender, EventArgs e)
        {
            if (!LoggedIn)
            {
                Client = new SimpleChatClient();

                bool Login = (Client.Login(textBox1.Text, textBox2.Text));
                if (Login)
                {
                    // Login successful, enable chatting etc.
                    textBox3.Enabled = true;
                    button3.Enabled = true;
                    this.Height = 570;
                    tabControl1.Visible = true;
                    LoggedIn = true;
                    button1.Text = "Logout";
                    button2.Enabled = false;
                }
                else
                    MessageBox.Show("Login failed.");
            }
            else
            {
                // logout, disable chatting etc.
                Client = new SimpleChatClient();
                Chats = new Dictionary<string, Chat>();
                tabControl1.TabPages.Clear();
                textBox1.Text = "";
                textBox2.Text = "";
                button2.Enabled = true;
                textBox3.Enabled = false;
                button3.Enabled = false;
                this.Height = 176;
                tabControl1.Visible = false;
                LoggedIn = false;
                button1.Text = "Login";
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            Client = new SimpleChatClient();
            string Register = Client.Register(textBox1.Text, textBox2.Text);
            if (Register == "Success")
            {
                MessageBox.Show("Account created.");
            }
            if (Register == "Existing")
            {
                MessageBox.Show("Username already in use.");
            }
        }

        private void button3_Click_1(object sender, EventArgs e)
        {
            ProvideChat(textBox3.Text);
        }

        private void ProvideChat(string name)
        {
            Chat Dummy;
            bool ChatExisting = Chats.TryGetValue(name, out Dummy);
            if (ChatExisting)
            {   // if chat already exists, make its tabpage active
                tabControl1.SelectedTab = Dummy.ChatPage;
                return;
            }
            else
            {
                // create new tabpage for the conversation
                TabPage NewPage = new TabPage(name);

                TextBox ChatWindow = new TextBox();
                ChatWindow.Left = 10;
                ChatWindow.Top = 10;
                ChatWindow.Width = 532;
                ChatWindow.Height = 180;
                ChatWindow.Multiline = true;
                ChatWindow.ScrollBars = ScrollBars.Vertical;
                ChatWindow.ReadOnly = false;
                NewPage.Controls.Add(ChatWindow);

                TextBox SendBox = new TextBox();
                SendBox.Left = 10;
                SendBox.Top = 200;
                SendBox.Width = 450;
                NewPage.Controls.Add(SendBox);
                SendBox.Name = "snd" + name;
                SendBox.Click += new EventHandler(SendBox_Click);
                SendBox.TextChanged += new EventHandler(SendBox_TextChanged);

                Button SendButton = new Button();
                SendButton.Left = 470;
                SendButton.Top = 200;
                SendButton.Text = "Send";
                SendButton.Name = "btn" + name;
                SendButton.Click += new EventHandler(SendButton_Click);
                NewPage.Controls.Add(SendButton);

                Chat NewChat = new Chat();
                NewChat.ChatWindow = ChatWindow;
                NewChat.SendBox = SendBox;
                NewChat.ChatPage = NewPage;
                NewChat.Partner = name;
                NewChat.SendButton = SendButton;

                NewPage.Name = "tpg" + name;
                tabControl1.SelectedIndexChanged += new EventHandler(tabControl1_SelectedIndexChanged);

                Chats.Add(name, NewChat);

                this.AcceptButton = NewChat.SendButton;

                if (tabControl1.InvokeRequired)
                {
                    tabControl1.Invoke(new Action(() =>
                    {
                        tabControl1.TabPages.Add(NewPage);
                        tabControl1.SelectedTab = NewPage;
                    }));
                }
                else
                {
                    tabControl1.TabPages.Add(NewPage);
                    tabControl1.SelectedTab = NewPage;
                }

                this.ActiveControl = NewChat.SendBox;
            }
        }

        private void tabControl1_SelectedIndexChanged(Object sender, EventArgs e)
        {
            // change tabpages / chats
            if (((TabControl)sender).SelectedIndex != -1)
            {
                string Receiver = ((TabControl)sender).TabPages[((TabControl)sender).SelectedIndex].Name.ToString().Substring(3, ((TabControl)sender).TabPages[((TabControl)sender).SelectedIndex].Name.ToString().Length - 3);
                this.AcceptButton = Chats[Receiver].SendButton;
                this.ActiveControl = Chats[Receiver].SendBox;
            }
        }

        private void SendBox_Click(Object sender, EventArgs e)
        {
            // click in textbox for sending
            string Receiver = ((TextBox)sender).Name.Substring(3, ((TextBox)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            CurrentChat.NewMessage = false;
            CurrentChat.ChatPage.Text = CurrentChat.Partner;
        }

        private void SendBox_TextChanged(Object sender, EventArgs e)
        {
            string Receiver = ((TextBox)sender).Name.Substring(3, ((TextBox)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            CurrentChat.NewMessage = false;
            CurrentChat.ChatPage.Text = CurrentChat.Partner;
        }

        private void SendButton_Click(Object sender, EventArgs e)
        {
            // send message
            string Receiver = ((Button)sender).Name.Substring(3, ((Button)sender).Name.Length - 3);
            Chat CurrentChat = Chats[Receiver];
            Client.Send(Receiver, CurrentChat.SendBox.Text);
            CurrentChat.ChatWindow.Text += Client.GetUsername() + ": " + CurrentChat.SendBox.Text + Environment.NewLine;
            CurrentChat.SendBox.Text = "";
            Chats[Receiver].ChatWindow.SelectionStart = Chats[Receiver].ChatWindow.TextLength;
            Chats[Receiver].ChatWindow.ScrollToCaret();
        }

        private void IncomingMessage(string NameSender, string message)
        {
            // rceive an incoming message and display it in the correct chat window
            ProvideChat(NameSender);

            Chats[NameSender].NewMessage = true;
            Chats[NameSender].ChatWindow.Invoke(new Action(() =>
            {
                Chats[NameSender].ChatWindow.Text += NameSender + ": " + message + Environment.NewLine;
                Chats[NameSender].ChatWindow.SelectionStart = Chats[NameSender].ChatWindow.TextLength;
                Chats[NameSender].ChatWindow.ScrollToCaret();
            }));
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            // periodically poll messages from server
            if (Client != null)
            {
                List<SimpleChatClient.Message> Messages = Client.Receive();
                foreach (SimpleChatClient.Message m in Messages)
                {
                    IncomingMessage(m.Sender, m.Msg);
                }
            }
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Height = 176;
        }

        private void timer2_Tick(object sender, EventArgs e)
        {
            // timer used for the blinking effect for new messages
            foreach (Chat c in Chats.Values)
            {
                if (c.NewMessage)
                {
                    string BlankName = "";
                    BlankName = BlankName.PadLeft(c.Partner.Length, ' ');
                    if (c.ChatPage.Text == BlankName)
                        c.ChatPage.Text = c.Partner;
                    else
                        c.ChatPage.Text = BlankName;
                }
            }
        }

        private void button4_Click(object sender, EventArgs e)
        {
            if (tabControl1.SelectedIndex != -1)
            {
                string Current = tabControl1.TabPages[tabControl1.SelectedIndex].Name.ToString().Substring(3, tabControl1.TabPages[tabControl1.SelectedIndex].Name.ToString().Length - 3);
                tabControl1.TabPages.RemoveAt(tabControl1.SelectedIndex);
                Chats.Remove(Current);
            }
        }
    }

    public class Chat
    {
        public TextBox ChatWindow;
        public TextBox SendBox;
        public bool NewMessage = false;
        public TabPage ChatPage;
        public string Partner;
        public Button SendButton;
    }
  
}

Form1.Designer.cs:

namespace SimpleChatClient
{
    partial class Form1
    {
        /// <summary>
       /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
       /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
       /// Required method for Designer support - do not modify
       /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.button2 = new System.Windows.Forms.Button();
            this.button1 = new System.Windows.Forms.Button();
            this.label2 = new System.Windows.Forms.Label();
            this.textBox2 = new System.Windows.Forms.TextBox();
            this.label1 = new System.Windows.Forms.Label();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.tabControl1 = new System.Windows.Forms.TabControl();
            this.button3 = new System.Windows.Forms.Button();
            this.textBox3 = new System.Windows.Forms.TextBox();
            this.timer1 = new System.Windows.Forms.Timer(this.components);
            this.timer2 = new System.Windows.Forms.Timer(this.components);
            this.button4 = new System.Windows.Forms.Button();
            this.SuspendLayout();
            //
            // button2
            //
            this.button2.Location = new System.Drawing.Point(125, 86);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(80, 25);
            this.button2.TabIndex = 11;
            this.button2.Text = "Register";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            //
            // button1
            //
            this.button1.Location = new System.Drawing.Point(25, 86);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(81, 25);
            this.button1.TabIndex = 10;
            this.button1.Text = "Login";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);
            //
            // label2
            //
            this.label2.AutoSize = true;
            this.label2.Location = new System.Drawing.Point(271, 49);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(53, 13);
            this.label2.TabIndex = 9;
            this.label2.Text = "Password";
            //
            // textBox2
            //
            this.textBox2.Location = new System.Drawing.Point(25, 49);
            this.textBox2.Name = "textBox2";
            this.textBox2.PasswordChar = '*';
            this.textBox2.Size = new System.Drawing.Size(219, 20);
            this.textBox2.TabIndex = 8;
            //
            // label1
            //
            this.label1.AutoSize = true;
            this.label1.Location = new System.Drawing.Point(271, 23);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(55, 13);
            this.label1.TabIndex = 7;
            this.label1.Text = "Username";
            //
            // textBox1
            //
            this.textBox1.Location = new System.Drawing.Point(25, 23);
            this.textBox1.Name = "textBox1";
            this.textBox1.Size = new System.Drawing.Size(219, 20);
            this.textBox1.TabIndex = 6;
            //
            // tabControl1
            //
            this.tabControl1.Dock = System.Windows.Forms.DockStyle.Bottom;
            this.tabControl1.Location = new System.Drawing.Point(0, 199);
            this.tabControl1.Name = "tabControl1";
            this.tabControl1.SelectedIndex = 0;
            this.tabControl1.Size = new System.Drawing.Size(804, 329);
            this.tabControl1.TabIndex = 12;
            this.tabControl1.Visible = false;
            //
            // button3
            //
            this.button3.Enabled = false;
            this.button3.Location = new System.Drawing.Point(274, 150);
            this.button3.Name = "button3";
            this.button3.Size = new System.Drawing.Size(77, 32);
            this.button3.TabIndex = 13;
            this.button3.Text = "Start Chat";
            this.button3.UseVisualStyleBackColor = true;
            this.button3.Click += new System.EventHandler(this.button3_Click_1);
            //
            // textBox3
            //
            this.textBox3.Enabled = false;
            this.textBox3.Location = new System.Drawing.Point(25, 157);
            this.textBox3.Name = "textBox3";
            this.textBox3.Size = new System.Drawing.Size(219, 20);
            this.textBox3.TabIndex = 14;
            //
            // timer1
            //
            this.timer1.Enabled = true;
            this.timer1.Interval = 1000;
            this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
            //
            // timer2
            //
            this.timer2.Enabled = true;
            this.timer2.Interval = 1000;
            this.timer2.Tick += new System.EventHandler(this.timer2_Tick);
            //
            // button4
            //
            this.button4.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
            this.button4.Location = new System.Drawing.Point(751, 157);
            this.button4.Name = "button4";
            this.button4.Size = new System.Drawing.Size(41, 26);
            this.button4.TabIndex = 15;
            this.button4.Text = "X";
            this.button4.UseVisualStyleBackColor = true;
            this.button4.Click += new System.EventHandler(this.button4_Click);
            //
            // Form1
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(804, 528);
            this.Controls.Add(this.button4);
            this.Controls.Add(this.textBox3);
            this.Controls.Add(this.button3);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.button1);
            this.Controls.Add(this.label2);
            this.Controls.Add(this.textBox2);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.tabControl1);
            this.Name = "Form1";
            this.Text = "SimpleChat";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.ResumeLayout(false);
            this.PerformLayout();

        }

        #endregion

        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.Label label2;
        private System.Windows.Forms.TextBox textBox2;
        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.TextBox textBox1;
        private System.Windows.Forms.TabControl tabControl1;
        private System.Windows.Forms.Button button3;
        private System.Windows.Forms.TextBox textBox3;
        private System.Windows.Forms.Timer timer1;
        private System.Windows.Forms.Timer timer2;
        private System.Windows.Forms.Button button4;
    }
}

Das komplette Projekt kann hier heruntergeladen werden, hier gibt es einen Link zu den Installationsdateien (durch Ausführen der Datei setup.exe wird das Programm installiert).
Ich heiße in dem Chat auch bloggeroliver (Groß- / Kleinschreibung ist wichtig) und freue mich auf eure Nachrichten!

Im nächsten Post wird, wie gesagt, das Problem der MySQL Injections angegangen, dort werde ich außerdem andere Sicherheitslücken dieses Chats beschreiben und deren Lösung zeigen.

Keine Kommentare:

Kommentar veröffentlichen