Merge pull request 'Security Update' (#6) from testing into main

Reviewed-on: #6
This commit is contained in:
raul 2024-05-17 08:00:12 +02:00
commit e48521b456
9 changed files with 267 additions and 107 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,22 +126,45 @@ 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")
if _, err := conn.Write([]byte(message)); err != nil {
@ -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)