diff --git a/.gitea/workflows/go-audit.yaml b/.gitea/workflows/go-audit.yaml deleted file mode 100644 index 70c5e4b..0000000 --- a/.gitea/workflows/go-audit.yaml +++ /dev/null @@ -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 ./... diff --git a/.gitignore b/.gitignore index 97ea58f..7368b61 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ *.so *.dylib dist/ +server.crt +server.key # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index c622535..b7d6f61 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,35 @@ Tiny IRC-like chat server written in Go

-## Usage examples -### Starting the server: -./mini-chat server --port 1337 --history chat.log +## Commands +### Client +``` +Example: + ./mini-chat client --ip 192.168.0.100 --port 1337 -### Connecting to the server: -./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") +``` diff --git a/cmd/client.go b/cmd/client.go index 0a52479..5ae7199 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -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 } diff --git a/cmd/clientFunc.go b/cmd/clientFunc.go index f8d2de8..85ef25e 100644 --- a/cmd/clientFunc.go +++ b/cmd/clientFunc.go @@ -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 { @@ -33,25 +37,69 @@ func (m Message) toSend() { } var ( - serverPort string = "1302" - serverIP string - data Message + 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) - sendName(conn) + 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) - } + g.Close() + 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 { - return "", err +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 } - finalMessage := string(serverMessage) - return finalMessage, nil + + 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 := 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 { diff --git a/cmd/gen-cert.sh b/cmd/gen-cert.sh new file mode 100755 index 0000000..d5b25f2 --- /dev/null +++ b/cmd/gen-cert.sh @@ -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!" diff --git a/cmd/root.go b/cmd/root.go index 57577e0..3557b46 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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") } diff --git a/cmd/server.go b/cmd/server.go index 2b13d0a..c7b0142 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -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 } diff --git a/cmd/serverFunc.go b/cmd/serverFunc.go index 97d9a10..74b679e 100644 --- a/cmd/serverFunc.go +++ b/cmd/serverFunc.go @@ -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 scanner.Scan() { - conn.Write([]byte(fmt.Sprintln(scanner.Text()))) + // 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)