Compare commits

..

54 Commits
v1.0.1 ... 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
raul 61e35fd758 Update README.md 2024-05-08 08:07:38 +02:00
raul bc8c852e4b Merge pull request 'Quality of Life update' from testing into main
Reviewed-on: #4
2024-05-08 08:04:44 +02:00
raul d821979b65 Only trigger CI/CD on PR
Go audit / audit (pull_request) Successful in 1m39s Details
2024-05-08 08:01:19 +02:00
raul cc96783a6c Update README.md
Go audit / audit (pull_request) Successful in 1m41s Details
2024-05-07 11:00:42 +02:00
raul 2ec372ad42 Add demo.gif 2024-05-07 10:48:54 +02:00
raul 158bf9373f Add history parameter to restore old chats 2024-05-07 10:48:16 +02:00
raul 9e1affdc6c Occupy rest of terminal space
After realizing what I'd have to go through to build a functional "Users
online:" view, I have decided to NOT do that, especially because this
entire thing still runs on raw TCP messages for commmunication and
trying to tell apart regular messages from join/disconnect messages in
the client to properly update the possible new view would be hell.
2024-05-07 09:49:34 +02:00
raul 370f217208 Add auto-scroll control 2024-05-06 14:09:47 +02:00
raul bcaa1c12af Add credits and current username to UI 2024-05-06 13:45:06 +02:00
raul f8332d102a Disable "Send it" button 2024-05-06 13:34:29 +02:00
raul c63028dea0 Add client TUI scrolling 2024-05-06 13:31:44 +02:00
raul 7eb991a4e4 Tweak chat dimensions 2024-04-27 21:46:00 +02:00
raul 3c79ebfbb4 Update README.md
Go audit / audit (push) Successful in 1m35s Details
2024-04-26 09:37:46 +02:00
raul 125502bab3 Merge pull request 'Refactored codebase' from testing into main
Go audit / audit (push) Successful in 1m37s Details
Reviewed-on: #2
2024-04-26 08:56:36 +02:00
raul 80da12cfcf Nuclear refactoring
Go audit / audit (pull_request) Successful in 1m48s Details
I've finally managed to properly rebuild the project, it's not extremely
clean, but compared to before, it's infinitely more functional and
expandable.
2024-04-26 08:48:48 +02:00
raul 3759701e22 Update README.md
Go audit / audit (push) Successful in 1m6s Details
2024-04-14 10:13:22 +02:00
raul 2a3bd788f8 Merge pull request 'Final PR' from testing into main
Go audit / audit (push) Successful in 1m7s Details
Reviewed-on: #1
2024-04-14 10:04:59 +02:00
raul cec33907b7 Removed currentUsers view
Go audit / audit (pull_request) Successful in 1m14s Details
I cannot handle this codebase anymore
2024-04-14 09:48:21 +02:00
raul c85fa5b031 Fixed catastrophic memory leak in client
Whenever a server would be stopped while the clients were connected to
it, the for loop handling the messages received by the server would
start a feedback loop as no more data could be read from "conn", thus
spawning an infinite number of byte arrays and crashing my laptop
2024-04-14 09:27:17 +02:00
raul ca10bc4284 Implement Gocui UI into mini-chat client 2024-04-13 13:11:36 +02:00
raul 75dac0ccef Attempt to clean up server codebase 2024-04-13 13:08:43 +02:00
raul 678244d289 Update dependencies 2024-04-13 13:08:02 +02:00
raul c4fb5606f6 Testing sending messages to server from client 2024-04-12 12:00:08 +02:00
raul 6599c66abb Add ui.go file 2024-04-12 11:59:48 +02:00
raul 39c2dde016 Update dependencies 2024-04-12 08:55:59 +02:00
raul 622340610a Create client.go 2024-04-02 11:27:48 +02:00
raul 55da7f8afc Fix README.md titles
Go audit / audit (push) Successful in 1m3s Details
2024-04-01 12:57:57 +02:00
13 changed files with 767 additions and 184 deletions

View File

@ -1,27 +0,0 @@
name: Go audit
run-name: ${{ gitea.actor }} pushed to main! 🚀
on:
push:
branches: [main]
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 *.so
*.dylib *.dylib
dist/ dist/
server.crt
server.key
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View File

@ -1,11 +1,40 @@
# mini-chat # mini-chat
Tiny IRC-like chat server built for compatibility with netcat and written in Go Tiny IRC-like chat server written in Go
# # Usage <p align="center">
<img width="90%" height="90%" src="https://git.bulgariu.xyz/raul/mini-chat/raw/branch/main/demo.gif"/>
</p>
# # # Starting the server ## Commands
./mini-chat server --port 1337 ### Client
```
Example:
./mini-chat client --ip 192.168.0.100 --port 1337
# # # Connecting to the server Usage:
nc $SERVER_IP $SERVER_PORT 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")
```

75
cmd/client.go Normal file
View File

@ -0,0 +1,75 @@
/*
Copyright © 2024 Raul <raul@bulgariu.xyz>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var clientCmd = &cobra.Command{
Use: "client",
Short: "Client interface for mini-chat",
Long: `Refactored mini-chat client that properly interfaces
with its server.
Example:
./mini-chat client --ip 192.168.0.100 --port 1337`,
Run: func(cmd *cobra.Command, args []string) {
if err := setClientParameters(cmd); err != nil {
cmd.Help()
fmt.Printf("\n-----------------------\n")
fmt.Println(err)
fmt.Printf("-----------------------\n")
os.Exit(0)
}
Client()
},
}
func init() {
rootCmd.AddCommand(clientCmd)
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 {
parameterPort, err := cmd.Flags().GetString("port")
if err != nil {
return err
}
if parameterPort != "" {
serverPort = parameterPort
}
parameterIP, err := cmd.Flags().GetString("ip")
if err != nil {
return err
}
if parameterIP == "" {
err := fmt.Errorf("IP cannot be empty")
return err
}
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
}

315
cmd/clientFunc.go Normal file
View File

@ -0,0 +1,315 @@
/*
Copyright © 2024 Raul <raul@bulgariu.xyz>
*/
package cmd
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"log"
"net"
"os"
"runtime"
"strings"
"time"
"github.com/jroimartin/gocui"
"github.com/nsf/termbox-go"
)
type Message struct {
Contents string
//Date time.Time
Server net.Conn
}
type ProfileData struct {
Username string
}
var Profile ProfileData
func (m Message) toSend() {
m.Server.Write([]byte(m.Contents))
}
var (
serverPort string = "1302"
serverIP string
data Message
clientInsecure bool
useASCII bool
)
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, 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()
}
////////////////////////////////////////////////////
// REFACTORING BELOW //
////////////////////////////////////////////////////
func listenMessages(g *gocui.Gui) {
time.Sleep(time.Millisecond * 50)
chatbox, err := g.View("chatbox")
if err != nil {
log.Panicln(err)
}
for {
messageFromServer, _, err := receiveMessage(data.Server)
if err != nil {
g.Close()
os.Exit(0)
}
formattedMessage := strings.TrimRight(messageFromServer, "\n")
fmt.Fprintln(chatbox, formattedMessage)
termbox.Interrupt()
}
}
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
}
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 := scanLine()
if err != nil {
log.Fatalf("Error occurred sending message to server: %v\n", err)
}
Profile.Username = strings.TrimRight(message, "\n\r")
if _, err := conn.Write([]byte(Profile.Username + "\n")); err != nil {
log.Fatalf("Error occurred writing to server: %v\n", err)
}
}
func GUI() {
g, err := gocui.NewGui(gocui.OutputNormal)
if err != nil {
log.Panicln(err)
}
defer g.Close()
g.SetManagerFunc(layout)
g.Mouse = true
g.Cursor = true
if useASCII == true {
g.ASCII = true
}
initKeybindings(g)
go listenMessages(g)
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
log.Panicln(err)
}
}
func quit(*gocui.Gui, *gocui.View) error {
return gocui.ErrQuit
}
func sendToServer(g *gocui.Gui, v *gocui.View) error {
textarea, err := g.View("textarea")
if err != nil {
log.Panicln(err)
}
message := textarea.Buffer()
msg := string(message)
if msg == "" {
return nil
}
if msg == "" {
return nil
}
data.Contents = msg
data.toSend()
textarea.Clear()
textarea.SetCursor(0, 0)
return nil
}
func scrollView(v *gocui.View, dy int) error {
if v != nil {
v.Autoscroll = false
ox, oy := v.Origin()
if err := v.SetOrigin(ox, oy+dy); err != nil {
return err
}
}
return nil
}
func autoscroll(g *gocui.Gui, v *gocui.View) error {
chatbox, err := g.View("chatbox")
if err != nil {
log.Panicf("Error happened setting autoscroll: %v\n", err)
}
chatbox.Autoscroll = true
return nil
}
func initKeybindings(g *gocui.Gui) error {
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
// if err := g.SetKeybinding("button", gocui.MouseLeft, gocui.ModNone, sendToServer); err != nil {
// log.Panicln(err)
// }
if err := g.SetKeybinding("textarea", gocui.KeyCtrlA, gocui.ModNone, autoscroll); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("textarea", gocui.KeyEnter, gocui.ModNone, sendToServer); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("chatbox", gocui.MouseWheelUp, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
scrollView(v, -1)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("chatbox", gocui.MouseWheelDown, gocui.ModNone,
func(g *gocui.Gui, v *gocui.View) error {
scrollView(v, 1)
return nil
}); err != nil {
log.Panicln(err)
}
return nil
}
func layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
if chatbox, err := g.SetView("chatbox", 2, 1, maxX-2, maxY-5); err != nil {
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 {
// if err != gocui.ErrUnknownView {
// return err
// }
//
// button.Wrap = true
// button.Frame = true
// fmt.Fprintln(button, "Send it")
// }
if textarea, err := g.SetView("textarea", 2, maxY-4, maxX-2, maxY-2); err != nil {
if err != gocui.ErrUnknownView {
return err
}
if _, err := g.SetCurrentView("textarea"); err != nil {
log.Panicln(err)
}
textarea.Title = "(" + Profile.Username + ") " + "Send message" + " (Ctrl+A: Autoscroll)"
textarea.Wrap = true
textarea.Editable = true
}
return 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

@ -1,32 +1,21 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package cmd package cmd
import ( import (
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
) )
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "mini-chat", Use: "mini-chat",
Short: "Minimalistic chat server built using Go", Short: "Application for hosting and joining a simple chat server",
Long: `mini-chat was originally designed to be used as a standalone server
users could connect to using netcat, but it can be also used with the binary
client
Examples: Long: `Application for hosting and joining a simple chat server`,
./mini-chat server --port 8080`,
// 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() { func Execute() {
err := rootCmd.Execute() err := rootCmd.Execute()
if err != nil { if err != nil {
@ -35,13 +24,4 @@ func Execute() {
} }
func init() { 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/.mini-chat.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

@ -1,150 +1,69 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package cmd package cmd
import ( import (
"bufio"
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"net" "log"
"strings"
"time"
) )
// serverCmd represents the server command
var serverCmd = &cobra.Command{ var serverCmd = &cobra.Command{
Use: "server", Use: "server",
Short: "Main chat server", Short: "Tiny chat server",
Long: `You can connect to this server by running the following command from Long: `Refactored mini-chat server designed to be connected to
a client: using a proper interface this time, not using netcat.
nc $SERVER_IP $PORT
Assuming your IP is 192.168.0.30 and the server port is left on default the Example:
command would be as follows: ./mini-chat server --port 1337 --history chat.log --password coolh4x0r1337`,
nc 192.168.0.30 1302`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
server(cmd)
if err := setServerParameters(cmd); err != nil {
log.Fatalf("Error happened trying to set parameters: %v\n", err)
}
Server()
}, },
} }
func init() { func init() {
rootCmd.AddCommand(serverCmd) rootCmd.AddCommand(serverCmd)
serverCmd.PersistentFlags().StringP("port", "p", "1302", "Port to use for listening")
// Here you will define your flags and configuration settings. serverCmd.PersistentFlags().StringP("history", "r", "", "File to store and recover chat history from")
serverCmd.PersistentFlags().String("password", "", "Password for accessing the chat server")
// Cobra supports Persistent Flags which will work for this command serverCmd.Flags().Bool("insecure", false, "[UNSAFE] Do not use TLS encryption")
// and all subcommands, e.g.:
// serverCmd.PersistentFlags().String("foo", "", "A help for foo")
// 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.Flags().String("port", "1302", "Port to listen on")
} }
var receiverIsStarted bool = false func setServerParameters(cmd *cobra.Command) error {
parameterPort, err := cmd.Flags().GetString("port")
type chatter struct {
Username string
IP string
}
func server(cmd *cobra.Command) {
var lport string
port, _ := cmd.Flags().GetString("port")
if port != "" {
lport = port
} else {
lport = "1302"
}
ln, err := net.Listen("tcp", ":"+lport)
cobra.CheckErr(err)
fmt.Printf("Listening on port %v...\n", lport)
masterChannel := make(chan string, 20)
slaveChannel := make(chan string, 20)
go masterReceiver(slaveChannel, masterChannel)
// TODO: get channels properly working
// receivingChannel := make(chan string)
// sendingChannel := make(chan string)
for {
conn, err := ln.Accept()
cobra.CheckErr(err)
numOfClients++
go handleConn(conn, slaveChannel, masterChannel)
}
}
var numOfClients int = 0
func masterReceiver(slaveChannel chan<- string, masterChannel <-chan string) {
for {
message := <-masterChannel
for i := 0; i < numOfClients; i++ {
slaveChannel <- message
}
}
}
// func receiver(conn net.Conn, ch chan string) {
// fmt.Println("THE RECEIVER HAS BEEN STARTED, YOU CAN ONLY SEE THIS MESSAGE ONCE")
// receiverIsStarted = false
// for {
// select {
// case otherMessage := <-ch:
// conn.Write([]byte(otherMessage))
// default:
// }
// }
// }
func handleConn(conn net.Conn, slaveChannel <-chan string, masterChannel chan<- string) {
defer conn.Close()
//var otherMessage string
go func() {
for {
//select {
message := <-slaveChannel
conn.Write([]byte(message))
}
}()
IP := getIP(conn)
t := time.Now()
date := fmt.Sprintf("%d-%02d-%02d | %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
final_message_connect := fmt.Sprintf("[%v] Received connection from %v!\n", date, IP)
fmt.Print(final_message_connect)
conn.Write([]byte("What's your name?\nName: "))
name, err := bufio.NewReader(conn).ReadString('\n')
if err != nil { if err != nil {
numOfClients-- return err
return
}
var NewUser = new(chatter)
NewUser.Username = strings.TrimRight(name, "\n")
NewUser.IP = IP
for {
message, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
fmt.Printf(NewUser.Username + " disconnected!\n")
numOfClients--
return
}
finalMessage := fmt.Sprint("{" + NewUser.IP + "} " + NewUser.Username + ": " + message)
fmt.Print(finalMessage)
masterChannel <- finalMessage
//conn.Write([]byte(message))
} }
if parameterPort != "" {
listenPort = parameterPort
} }
func getIP(conn net.Conn) (IP string) { parameterHistory, err := cmd.Flags().GetString("history")
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { if err != nil {
IP = fmt.Sprintf("%v", addr) return err
} }
return IP if parameterHistory != "" {
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
} }

264
cmd/serverFunc.go Normal file
View File

@ -0,0 +1,264 @@
/*
Copyright © 2024 Raul <raul@bulgariu.xyz>
*/
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
}
func (u User) CreateUser(usr string, ip string) User {
u.Username = usr
u.IP = ip
return u
}
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)
}
defer ln.Close()
fmt.Printf("Listening on port %v...\n", listenPort)
for {
conn, err := ln.Accept()
if err != nil {
log.Fatalf("Error happened trying to accept connection: %v\n", err)
}
chatChan := make(chan string, 30)
listenerList = append(listenerList, chatChan)
go handleConn(conn, chatChan)
}
}
func getUsername(conn net.Conn) (s string, err error) {
conn.Write([]byte("What's your name?\nChoice: "))
name, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return "", err
}
trimmedName := strings.TrimRight(name, "\n")
return trimmedName, nil
}
func getUserInput(conn net.Conn) (s string, err error) {
message, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return "", err
}
return message, nil
}
func removeFromList(chatChan chan string) {
for i, v := range listenerList {
if v == chatChan {
listenerList = append(listenerList[:i], listenerList[:i+1]...)
}
}
}
func populateChat(conn net.Conn) {
if isLogging == false {
return
}
file, err := os.Open(logLocation)
if err != nil {
log.Printf("Error opening file for populating: %v\n", err)
}
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)
//////////////////////////////////
// Get user information
//////////////////////////////////
userName, err := getUsername(conn)
if err != nil {
log.Printf("Error occurred getting username: %v\n", err)
//removeFromList(chatChan)
return
}
userIP := getIP(conn)
//////////////////////////////////
populateChat(conn)
newUserTemplate := new(User)
newUser := newUserTemplate.CreateUser(userName, userIP)
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)
//////////////////////////////////
for {
message, err := getUserInput(conn)
if err != nil {
quitMessage := fmt.Sprintf("[%v] %v has disconnected!", getTime(), newUser.Username)
fmt.Println(quitMessage + " [" + newUser.IP + "]")
addToLog(fmt.Sprintln(quitMessage))
sendMessage(quitMessage)
//removeFromList(chatChan)
// if _, err := conn.Write([]byte(quitMessage)); err != nil {
// log.Printf("Error happened sending disconnect message: %v", err)
// }
return
}
finalMessage := fmt.Sprintf("[%v] %v: %v", getTime(), newUser.Username, strings.TrimRight(message, "\n"))
fm := fmt.Sprintf("%v\n", finalMessage)
fmt.Print(fm)
addToLog(fm)
sendMessage(finalMessage)
//chatChan <- finalMessage
// if _, err := conn.Write([]byte(finalMessage)); err != nil {
// log.Printf("Error happened sending message: %v", err)
// }
}
//////////////////////////////////
}
func sendMessage(msg string) {
for _, ch := range listenerList {
ch <- msg
}
}
func receiveMessageServer(conn net.Conn, chatChan chan string) {
for {
select {
case message := <-chatChan:
conn.Write([]byte(message))
}
}
}
func getIP(conn net.Conn) (IP string) {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
IP = fmt.Sprintf("%v", addr.IP)
}
return IP
}
func addToLog(s string) {
if isLogging == false {
return
}
file, err := os.OpenFile(logLocation, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0640)
if err != nil {
log.Printf("Error occurred: %v\n", err)
}
defer file.Close()
file.WriteString(s)
}

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

10
go.mod
View File

@ -1,10 +1,16 @@
module mini-chat module mini-chat
go 1.22.1 go 1.22.2
require github.com/spf13/cobra v1.8.0 require (
github.com/jroimartin/gocui v0.5.0
github.com/nsf/termbox-go v1.1.1
github.com/spf13/cobra v1.8.0
)
require ( require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
) )

10
go.sum
View File

@ -1,6 +1,16 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jroimartin/gocui v0.5.0 h1:DCZc97zY9dMnHXJSJLLmx9VqiEnAj0yh0eTNpuEtG/4=
github.com/jroimartin/gocui v0.5.0/go.mod h1:l7Hz8DoYoL6NoYnlnaX6XCNR62G7J5FfSW5jEogzaxE=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=

View File

@ -1,5 +1,5 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package main package main