Compare commits

..

27 Commits
v1.0.6 ... main

Author SHA1 Message Date
raul e3bd047d86 Fix Windows client not properly sending username 2024-05-27 10:40:59 +02:00
raul e48521b456 Merge pull request 'Security Update' (#6) from testing into main
Reviewed-on: #6
2024-05-17 08:00:12 +02:00
raul c837e78c7c Remove workflow file
The damn runner keeps making too many POST requests per minute to my
Gitea instance and flooding the entire access.log file, I'm gonna keep
using Goreleaser for easy binary releases but CI/CD unfortunately has to
go away
2024-05-17 07:59:04 +02:00
raul ff801f81d0 Final tweaks
Go audit / audit (pull_request) Has been cancelled Details
2024-05-16 08:10:19 +02:00
raul c7a34a7d93 Add option to render client UI in pure ASCII 2024-05-14 16:31:36 +02:00
raul b2f9ff5016 Small tweaks 2024-05-14 15:59:23 +02:00
raul 93c76f4242 Add chatbox text wrapping
How the hell did I manage to forget about this?
2024-05-14 15:42:03 +02:00
raul a21199a6e0 Optimize client RAM memory usage
The receiveMessage() function would regularly keep casting strings based
on a 2048 byte array, this didn't seemingly pose a problem until I
noticed the RAM usage go through the roof when the client had to be
populated with the chat history of a lengthy chat log by the server. To
fix this I am now creating a second array using the number of bytes
being returned by the Read() method and copying the 2048 byte array's
contents into it, setting the former array to nil afterwards.
2024-05-14 15:31:55 +02:00
raul 5bb37a53fe Add date formatting to messages
I also went ahead and removed adding the user's IP to each one of its
messages to retain the user's privacy and security.
2024-05-14 15:30:16 +02:00
raul e7aada9a2a Update README.md 2024-05-14 10:05:02 +02:00
raul 6d15f303b7 Fix extra newlines being appended by populateChat() on TLS based servers
For some reason, writing to a conn variable on a TLS connection
appends newlines by default, so new users will have their chat box
populated if the server is using --history/-r with messages that have
unnecessary newlines
2024-05-14 09:58:09 +02:00
raul 872523700b Minor formatting changes 2024-05-14 09:27:23 +02:00
raul 7493af68fc Implement choosing TLS/plaintext for client 2024-05-14 09:26:45 +02:00
raul afafb12663 Implement choosing TLS/plaintext for server 2024-05-14 09:11:22 +02:00
raul 51b0dd5258 Implement clientside TLS 2024-05-14 09:07:00 +02:00
raul 0f33d13d6a Implement serverside TLS 2024-05-14 09:06:45 +02:00
raul e3b681e904 Add gen-cert.sh 2024-05-14 09:06:24 +02:00
raul fde175401e Update .gitignore 2024-05-14 08:19:39 +02:00
raul cc3f4fbcd6 Update README.md 2024-05-13 13:08:37 +02:00
raul e0f9c63a54 Reduce latency time for synchronization 2024-05-13 13:04:45 +02:00
raul 940d1287ec Stop showing errors on client Ctrl+C 2024-05-13 12:04:20 +02:00
raul 015e1bb65f Handle passwords clientside 2024-05-13 12:02:32 +02:00
raul 99923c64b1 Handle passwords serverside 2024-05-13 12:02:21 +02:00
raul 8cb40303dc Add password parameter 2024-05-13 12:02:03 +02:00
raul a615cb9194 Fix windows version being unable to take input 2024-05-10 09:50:26 +02:00
raul b6e133c608 Clean up Cobra files 2024-05-10 07:58:12 +02:00
raul 2571936a48 Add shorthand parameters 2024-05-08 08:34:07 +02:00
9 changed files with 269 additions and 109 deletions

View File

@ -1,25 +0,0 @@
name: Go audit
run-name: ${{ gitea.actor }} pulled to main! 🚀
on:
pull_request:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: '>=1.22.0'
- name: Verify dependencies
run: go mod verify
- name: Build
run: go build -v ./...
- name: Run go vet
run: go vet ./...

2
.gitignore vendored
View File

@ -9,6 +9,8 @@
*.so
*.dylib
dist/
server.crt
server.key
# Test binary, built with `go test -c`
*.test

View File

@ -6,9 +6,35 @@ Tiny IRC-like chat server written in Go
<img width="90%" height="90%" src="https://git.bulgariu.xyz/raul/mini-chat/raw/branch/main/demo.gif"/>
</p>
## Usage examples
### Starting the server:
./mini-chat server --port 1337 --history chat.log
### Connecting to the server:
## Commands
### Client
```
Example:
./mini-chat client --ip 192.168.0.100 --port 1337
Usage:
mini-chat client [flags]
Flags:
-a, --ascii Render UI in pure ASCII, might help with rendering issues
-h, --help help for client
--insecure [UNSAFE] Do not use TLS encryption
-i, --ip string Server IP to connect to
-p, --port string Server port to connect to (default "1302")
```
### Server
```
Example:
./mini-chat server --port 1337 --history chat.log --password coolh4x0r1337
Usage:
mini-chat server [flags]
Flags:
-h, --help help for server
-r, --history string File to store and recover chat history from
--insecure [UNSAFE] Do not use TLS encryption
--password string Password for accessing the chat server
-p, --port string Port to use for listening (default "1302")
```

View File

@ -10,7 +10,6 @@ import (
"os"
)
// clientCmd represents the client command
var clientCmd = &cobra.Command{
Use: "client",
Short: "Client interface for mini-chat",
@ -35,18 +34,10 @@ Example:
func init() {
rootCmd.AddCommand(clientCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// clientCmd.PersistentFlags().String("foo", "", "A help for foo")
clientCmd.PersistentFlags().String("ip", "", "Server IP to connect to")
clientCmd.PersistentFlags().String("port", "1302", "Server port to connect to")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// clientCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
clientCmd.PersistentFlags().StringP("ip", "i", "", "Server IP to connect to")
clientCmd.PersistentFlags().StringP("port", "p", "1302", "Server port to connect to")
clientCmd.Flags().Bool("insecure", false, "[UNSAFE] Do not use TLS encryption")
clientCmd.Flags().BoolP("ascii", "a", false, "Render UI in pure ASCII, might help with rendering issues")
}
func setClientParameters(cmd *cobra.Command) error {
@ -68,5 +59,17 @@ func setClientParameters(cmd *cobra.Command) error {
}
serverIP = parameterIP
insecure, err := cmd.Flags().GetBool("insecure")
if insecure == true {
clientInsecure = true
}
ascii, err := cmd.Flags().GetBool("ascii")
if err != nil {
return err
}
if ascii == true {
useASCII = true
}
return nil
}

View File

@ -6,14 +6,18 @@ package cmd
import (
"bufio"
"crypto/tls"
"fmt"
"github.com/jroimartin/gocui"
"github.com/nsf/termbox-go"
"io"
"log"
"net"
"os"
"runtime"
"strings"
"time"
"github.com/jroimartin/gocui"
"github.com/nsf/termbox-go"
)
type Message struct {
@ -36,22 +40,66 @@ var (
serverPort string = "1302"
serverIP string
data Message
clientInsecure bool
useASCII bool
)
func Client() {
func startSecureConnection() (net.Conn, error) {
conf := &tls.Config{
InsecureSkipVerify: true,
}
conn, err := tls.Dial("tcp", serverIP+":"+serverPort, conf)
return conn, err
}
func startInsecureConnection() (net.Conn, error) {
conn, err := net.Dial("tcp", serverIP+":"+serverPort)
return conn, err
}
func Client() {
var conn net.Conn
var err error
if clientInsecure == true {
fmt.Println("WARNING: Starting unencrypted connection!")
conn, err = startInsecureConnection()
} else {
conn, err = startSecureConnection()
}
if err != nil {
log.Fatalf("Error occurred trying to connect to server: %v\n", err)
}
defer conn.Close()
data.Server = conn
nameRequest, err := receiveMessage(conn)
nameRequest, b, err := receiveMessage(conn)
if err != nil {
log.Fatalf("Error occurred reading from server while requesting name: %v\n", err)
}
fmt.Print(nameRequest)
test := make([]byte, b)
copy(test, "Password: ")
if nameRequest == string(test) {
pass, err := scanLine()
if err != nil {
log.Fatal(err)
}
conn.Write([]byte(pass))
nameRequest, _, err := receiveMessage(conn)
if err != nil {
if err != io.EOF {
log.Fatalf("Error occurred reading from server while requesting name: %v\n", err)
}
os.Exit(1)
}
fmt.Print(nameRequest)
sendName(conn)
} else {
sendName(conn)
}
GUI()
}
@ -60,21 +108,16 @@ func Client() {
////////////////////////////////////////////////////
func listenMessages(g *gocui.Gui) {
time.Sleep(time.Millisecond * 250)
time.Sleep(time.Millisecond * 50)
chatbox, err := g.View("chatbox")
if err != nil {
log.Panicln(err)
}
for {
messageFromServer, err := receiveMessage(data.Server)
messageFromServer, _, err := receiveMessage(data.Server)
if err != nil {
// Avoid triggering an error if client quits the client
if err == gocui.ErrQuit {
g.Close()
} else {
g.Close()
log.Fatalf("Error occurred reading from server: %v\n", err)
}
os.Exit(0)
}
formattedMessage := strings.TrimRight(messageFromServer, "\n")
@ -83,25 +126,48 @@ func listenMessages(g *gocui.Gui) {
}
}
func receiveMessage(conn net.Conn) (s string, err error) {
serverMessage := make([]byte, 2048)
if _, err := conn.Read(serverMessage); err != nil {
func receiveMessage(conn net.Conn) (s string, b int, err error) {
serverMessage := make([]byte, 1536)
n, err := conn.Read(serverMessage)
if err != nil {
return "", 0, err
}
serverMessageReduced := make([]byte, n)
copy(serverMessageReduced, serverMessage)
finalMessage := string(serverMessageReduced)
serverMessage = nil
return finalMessage, n, nil
}
func scanLine() (line string, err error) {
switch runtime.GOOS {
case "linux":
message, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", err
}
finalMessage := string(serverMessage)
return finalMessage, nil
line = message
case "windows":
message, err := bufio.NewReader(os.Stdin).ReadString('\r')
if err != nil {
return "", err
}
line = message
}
return line, nil
}
func sendName(conn net.Conn) {
message, err := bufio.NewReader(os.Stdin).ReadString('\n')
message, err := scanLine()
if err != nil {
log.Fatalf("Error occurred sending message to server: %v\n", err)
}
Profile.Username = strings.TrimRight(message, "\n\r")
Profile.Username = strings.TrimRight(message, "\n")
if _, err := conn.Write([]byte(message)); err != nil {
if _, err := conn.Write([]byte(Profile.Username + "\n")); err != nil {
log.Fatalf("Error occurred writing to server: %v\n", err)
}
}
@ -115,6 +181,9 @@ func GUI() {
g.SetManagerFunc(layout)
g.Mouse = true
g.Cursor = true
if useASCII == true {
g.ASCII = true
}
initKeybindings(g)
go listenMessages(g)
@ -214,8 +283,10 @@ func layout(g *gocui.Gui) error {
if err != gocui.ErrUnknownView {
return err
}
chatbox.Autoscroll = true
chatbox.Title = "Chat Box (Find source at https://git.bulgariu.xyz/raul/mini-chat!)"
chatbox.Wrap = true
}
// if button, err := g.SetView("button", maxX/2+32, maxY-4, maxX-28, maxY-2); err != nil {

10
cmd/gen-cert.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
SSLCOMMAND=$(which openssl)
echo "[+] Generating server.key..."
$SSLCOMMAND genrsa -out server.key 2048
echo "[+] Generating server.crt..."
$SSLCOMMAND req -new -x509 -sha256 -key server.key -out server.crt -days 3650 -subj "/C=ES/ST=Valencia/L=Valencia/O=mini-chat /OU=mini-chat/CN=mini-chat"
echo "[+] Success!"

View File

@ -9,19 +9,13 @@ import (
"os"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "mini-chat",
Short: "Application for hosting and joining a simple chat server",
Long: `Application for hosting and joining a simple chat server`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
@ -30,13 +24,4 @@ func Execute() {
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.chat-tests.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -9,7 +9,6 @@ import (
"log"
)
// serverCmd represents the server command
var serverCmd = &cobra.Command{
Use: "server",
Short: "Tiny chat server",
@ -17,7 +16,7 @@ var serverCmd = &cobra.Command{
using a proper interface this time, not using netcat.
Example:
./mini-chat server --port 1337`,
./mini-chat server --port 1337 --history chat.log --password coolh4x0r1337`,
Run: func(cmd *cobra.Command, args []string) {
if err := setServerParameters(cmd); err != nil {
@ -30,18 +29,10 @@ Example:
func init() {
rootCmd.AddCommand(serverCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// serverCmd.PersistentFlags().String("foo", "", "A help for foo")
serverCmd.PersistentFlags().String("port", "1302", "port to use for listening")
serverCmd.PersistentFlags().String("history", "", "File to store and recover chat history from")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
serverCmd.PersistentFlags().StringP("port", "p", "1302", "Port to use for listening")
serverCmd.PersistentFlags().StringP("history", "r", "", "File to store and recover chat history from")
serverCmd.PersistentFlags().String("password", "", "Password for accessing the chat server")
serverCmd.Flags().Bool("insecure", false, "[UNSAFE] Do not use TLS encryption")
}
func setServerParameters(cmd *cobra.Command) error {
@ -52,6 +43,7 @@ func setServerParameters(cmd *cobra.Command) error {
if parameterPort != "" {
listenPort = parameterPort
}
parameterHistory, err := cmd.Flags().GetString("history")
if err != nil {
return err
@ -60,5 +52,18 @@ func setServerParameters(cmd *cobra.Command) error {
logLocation = parameterHistory
isLogging = true
}
parPassword, err := cmd.Flags().GetString("password")
if err != nil {
return err
}
if parPassword != "" {
password = parPassword
}
insecure, err := cmd.Flags().GetBool("insecure")
if insecure == true {
servInsecure = true
}
return nil
}

View File

@ -6,20 +6,29 @@ package cmd
import (
"bufio"
"crypto/tls"
_ "embed"
"fmt"
"log"
"net"
"os"
"os/exec"
"strings"
"time"
)
var (
listenPort string = "1302"
password string = ""
isLogging bool = false
logLocation string
listenerList []chan string
servInsecure bool
)
//go:embed gen-cert.sh
var script string
type User struct {
Username string
IP string
@ -31,8 +40,52 @@ func (u User) CreateUser(usr string, ip string) User {
return u
}
func Server() {
func createCerts() {
fmt.Println("[-] Certificates don't exist! Creating them...")
c := exec.Command("bash")
c.Stdin = strings.NewReader(script)
b, err := c.Output()
if err != nil {
log.Fatalf("Error occurred creating certificates: %v\n", err)
}
fmt.Print(string(b))
}
func startInsecureServer() (net.Listener, error) {
ln, err := net.Listen("tcp", ":"+listenPort)
return ln, err
}
func startSecureServer() (net.Listener, error) {
cer, err := tls.LoadX509KeyPair("server.crt", "server.key")
if os.IsNotExist(err) {
createCerts()
cer, err = tls.LoadX509KeyPair("server.crt", "server.key")
}
if err != nil {
log.Fatalf("Error happened loading certificates: %v\n", err)
}
config := &tls.Config{Certificates: []tls.Certificate{cer}}
ln, err := tls.Listen("tcp", ":"+listenPort, config)
return ln, err
}
func getTime() string {
t := time.Now()
currentTime := fmt.Sprintf("%d-%02d-%02d | %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
return currentTime
}
func Server() {
var ln net.Listener
var err error
if servInsecure == true {
fmt.Println("WARNING: Starting unencrypted server!")
ln, err = startInsecureServer()
} else {
ln, err = startSecureServer()
}
if err != nil {
log.Fatalf("Error happened trying to listen on port: %v\n", err)
}
@ -43,7 +96,7 @@ func Server() {
if err != nil {
log.Fatalf("Error happened trying to accept connection: %v\n", err)
}
chatChan := make(chan string, 10)
chatChan := make(chan string, 30)
listenerList = append(listenerList, chatChan)
go handleConn(conn, chatChan)
}
@ -86,15 +139,45 @@ func populateChat(conn net.Conn) {
}
defer file.Close()
scanner := bufio.NewScanner(file)
// For WHATEVER reason, writing to a TLS-based conn here appends newlines by default,
// so we have to split it off here to avoid ending up with chat logs full of
// unnecessary newlines
if servInsecure == true {
for scanner.Scan() {
conn.Write([]byte(fmt.Sprintln(scanner.Text())))
}
} else {
for scanner.Scan() {
conn.Write([]byte(fmt.Sprint(scanner.Text())))
}
}
}
func getPasswd(conn net.Conn) error {
conn.Write([]byte("Password: "))
userPassNewline, err := bufio.NewReader(conn).ReadString('\n')
userPass := strings.TrimRight(userPassNewline, "\n")
if err != nil {
e := fmt.Errorf("Node %v didn't respond to password prompt!\n", getIP(conn))
return e
}
if userPass != password {
e := fmt.Errorf("Node %v attempted connecting with an incorrect password!\n", getIP(conn))
return e
}
return nil
}
func handleConn(conn net.Conn, chatChan chan string) {
defer conn.Close()
if password != "" {
if err := getPasswd(conn); err != nil {
log.Print(err)
return
}
}
go receiveMessageServer(conn, chatChan)
//////////////////////////////////
@ -112,8 +195,8 @@ func handleConn(conn net.Conn, chatChan chan string) {
newUserTemplate := new(User)
newUser := newUserTemplate.CreateUser(userName, userIP)
joinMessage := fmt.Sprintf("%v has joined the chat!", newUser.Username)
fmt.Println(joinMessage)
joinMessage := fmt.Sprintf("[%v] %v has joined the chat!", getTime(), newUser.Username)
fmt.Println(joinMessage + " [" + newUser.IP + "]")
addToLog(fmt.Sprintln(joinMessage))
//conn.Write([]byte(joinMessage))
sendMessage(joinMessage)
@ -122,8 +205,8 @@ func handleConn(conn net.Conn, chatChan chan string) {
for {
message, err := getUserInput(conn)
if err != nil {
quitMessage := fmt.Sprintf("%v has disconnected!", newUser.Username)
fmt.Println(quitMessage)
quitMessage := fmt.Sprintf("[%v] %v has disconnected!", getTime(), newUser.Username)
fmt.Println(quitMessage + " [" + newUser.IP + "]")
addToLog(fmt.Sprintln(quitMessage))
sendMessage(quitMessage)
//removeFromList(chatChan)
@ -132,7 +215,7 @@ func handleConn(conn net.Conn, chatChan chan string) {
// }
return
}
finalMessage := fmt.Sprintf("[%v] %v: %v", newUser.IP, newUser.Username, strings.TrimRight(message, "\n"))
finalMessage := fmt.Sprintf("[%v] %v: %v", getTime(), newUser.Username, strings.TrimRight(message, "\n"))
fm := fmt.Sprintf("%v\n", finalMessage)
fmt.Print(fm)
addToLog(fm)