initial commit yo
This commit is contained in:
parent
daa6daa056
commit
ecc75a08c0
101
README.md
101
README.md
@ -1,2 +1,101 @@
|
||||
# gBBS
|
||||
# GBBS (Go Bulletin Board System)
|
||||
|
||||
GBBS is a modern implementation of a classic Bulletin Board System (BBS) written in Go. It provides a nostalgic interface with modern backend technologies, supporting Telnet, SSH, and Web access.
|
||||
|
||||
## Features
|
||||
|
||||
- Multi-protocol support: Telnet, SSH, and Web
|
||||
- User authentication and registration
|
||||
- Message board functionality
|
||||
- ANSI color support for Telnet and SSH clients
|
||||
- Customizable welcome screen
|
||||
- SQLite database for user management
|
||||
- Concurrent connections handling
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.16 or later
|
||||
- SQLite
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/strangeprogram/gbbs.git
|
||||
cd gbbs
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
3. Create a `config.json` file in the project root:
|
||||
```json
|
||||
{
|
||||
"telnet_port": 2323,
|
||||
"ssh_port": 2222,
|
||||
"web_port": 8080,
|
||||
"guestbook_path": "guestbook.txt",
|
||||
"web_root": "web",
|
||||
"welcome_screen_path": "welcome.ans"
|
||||
}
|
||||
```
|
||||
|
||||
4. Create a `welcome.ans` file with your desired ANSI art welcome screen.
|
||||
|
||||
### Running the BBS
|
||||
|
||||
To run the BBS in debug mode:
|
||||
|
||||
```
|
||||
go run cmd/gbbs/main.go --debug
|
||||
```
|
||||
|
||||
To compile and run:
|
||||
|
||||
```
|
||||
go build -o gbbs cmd/gbbs/main.go
|
||||
./gbbs
|
||||
```
|
||||
|
||||
## Connecting to the BBS
|
||||
|
||||
- Telnet: `telnet localhost 2323`
|
||||
- SSH: `ssh localhost -p 2222`
|
||||
- Web: Open a browser and navigate to `http://localhost:8080`
|
||||
|
||||
## Version History
|
||||
|
||||
- v0.1: Initial implementation with basic Telnet support
|
||||
- v0.2: Added SSH support
|
||||
- v0.3: Implemented web interface
|
||||
- v0.4: Added user authentication and registration
|
||||
- v0.5: Introduced message board functionality
|
||||
- v0.6: Improved ANSI color support and welcome screen customization
|
||||
- v0.7: Fixed SSH input handling issues
|
||||
|
||||
## TODO
|
||||
|
||||
- [ ] Implement IRC link integration
|
||||
- [ ] Add file transfer capabilities
|
||||
- [ ] Create a more robust web interface
|
||||
- [ ] Implement user roles and permissions
|
||||
- [ ] Add support for multiple message boards/forums
|
||||
- [ ] Implement private messaging between users
|
||||
- [ ] Create a plugin system for easy feature extensions
|
||||
- [ ] Add support for external authentication methods (e.g., OAuth)
|
||||
- [ ] Implement a basic game or interactive feature
|
||||
- [ ] Create a telnet/SSH client specifically designed for this BBS
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- MysticBBS | Oblivion/v2
|
||||
- BBS
|
||||
- archive the dream
|
||||
|
105
cmd/gbbs/main.go
Normal file
105
cmd/gbbs/main.go
Normal file
@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"gbbs/internal/config"
|
||||
"gbbs/internal/messageboard"
|
||||
"gbbs/internal/ssh"
|
||||
"gbbs/internal/telnet"
|
||||
"gbbs/internal/user"
|
||||
"gbbs/internal/web"
|
||||
)
|
||||
|
||||
var debug = flag.Bool("debug", false, "Enable debug mode")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *debug {
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
|
||||
} else {
|
||||
log.SetOutput(os.NewFile(0, os.DevNull))
|
||||
}
|
||||
|
||||
// Get the executable path
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get executable path: %v", err)
|
||||
}
|
||||
exePath := filepath.Dir(ex)
|
||||
|
||||
// Get the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get current working directory: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Executable directory: %s", exePath)
|
||||
log.Printf("Current working directory: %s", cwd)
|
||||
|
||||
// Try to load config from multiple locations
|
||||
configPaths := []string{
|
||||
filepath.Join(cwd, "config.json"),
|
||||
filepath.Join(cwd, "cmd", "gbbs", "config.json"),
|
||||
filepath.Join(exePath, "config.json"),
|
||||
filepath.Join(exePath, "cmd", "gbbs", "config.json"),
|
||||
}
|
||||
|
||||
var cfg *config.Config
|
||||
var configErr error
|
||||
for _, path := range configPaths {
|
||||
cfg, configErr = config.Load(path)
|
||||
if configErr == nil {
|
||||
log.Printf("Loaded configuration from: %s", path)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if configErr != nil {
|
||||
log.Fatalf("Failed to load configuration: %v\nTried paths: %v", configErr, configPaths)
|
||||
}
|
||||
|
||||
log.Printf("Loaded configuration: %+v", cfg)
|
||||
|
||||
userManager, err := user.NewManager("bbs.db")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize user manager: %v", err)
|
||||
}
|
||||
defer userManager.Close()
|
||||
|
||||
messageBoard, err := messageboard.New(cfg.GuestbookPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize message board: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := telnet.Serve(cfg, userManager, messageBoard); err != nil {
|
||||
log.Printf("Telnet server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := ssh.Serve(cfg, userManager, messageBoard); err != nil {
|
||||
log.Printf("SSH server error: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := web.Serve(cfg.WebPort, cfg.WebRoot, userManager, messageBoard); err != nil {
|
||||
log.Printf("Web server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("BBS is running. Telnet: %d, SSH: %d, Web: %d", cfg.TelnetPort, cfg.SSHPort, cfg.WebPort)
|
||||
|
||||
wg.Wait()
|
||||
}
|
8
config.json
Normal file
8
config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"telnet_port": 31337,
|
||||
"ssh_port": 22,
|
||||
"web_port": 8080,
|
||||
"guestbook_path": "guestbook.txt",
|
||||
"web_root": "web",
|
||||
"welcome_screen_path": "welcome.ans"
|
||||
}
|
0
guestbook.txt
Normal file
0
guestbook.txt
Normal file
13
internal/ansi/ansi.go
Normal file
13
internal/ansi/ansi.go
Normal file
@ -0,0 +1,13 @@
|
||||
package ansi
|
||||
|
||||
const (
|
||||
ColorBlack = "\033[0;30m"
|
||||
ColorRed = "\033[0;31m"
|
||||
ColorGreen = "\033[0;32m"
|
||||
ColorYellow = "\033[0;33m"
|
||||
ColorBlue = "\033[0;34m"
|
||||
ColorMagenta = "\033[0;35m"
|
||||
ColorCyan = "\033[0;36m"
|
||||
ColorWhite = "\033[0;37m"
|
||||
ColorReset = "\033[0m"
|
||||
)
|
52
internal/config/config.go
Normal file
52
internal/config/config.go
Normal file
@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
TelnetPort int `json:"telnet_port"`
|
||||
SSHPort int `json:"ssh_port"`
|
||||
WebPort int `json:"web_port"`
|
||||
GuestbookPath string `json:"guestbook_path"`
|
||||
WebRoot string `json:"web_root"`
|
||||
WelcomeScreenPath string `json:"welcome_screen_path"`
|
||||
}
|
||||
|
||||
func Load(configPath string) (*Config, error) {
|
||||
cfg := &Config{
|
||||
TelnetPort: 2323,
|
||||
SSHPort: 2222,
|
||||
WebPort: 8080,
|
||||
GuestbookPath: "guestbook.txt",
|
||||
WebRoot: "web",
|
||||
WelcomeScreenPath: "welcome.ans",
|
||||
}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := json.NewDecoder(file).Decode(cfg); err != nil {
|
||||
return nil, fmt.Errorf("error decoding config file: %v", err)
|
||||
}
|
||||
|
||||
// Convert relative paths to absolute paths
|
||||
cfg.GuestbookPath = makeAbsolute(filepath.Dir(configPath), cfg.GuestbookPath)
|
||||
cfg.WebRoot = makeAbsolute(filepath.Dir(configPath), cfg.WebRoot)
|
||||
cfg.WelcomeScreenPath = makeAbsolute(filepath.Dir(configPath), cfg.WelcomeScreenPath)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func makeAbsolute(basePath, path string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
}
|
||||
return filepath.Join(basePath, path)
|
||||
}
|
68
internal/messageboard/messageboard.go
Normal file
68
internal/messageboard/messageboard.go
Normal file
@ -0,0 +1,68 @@
|
||||
package messageboard
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MessageBoard struct {
|
||||
filePath string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func New(filePath string) (*MessageBoard, error) {
|
||||
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
|
||||
return &MessageBoard{filePath: filePath}, nil
|
||||
}
|
||||
|
||||
func (mb *MessageBoard) PostMessage(username, message string) error {
|
||||
if len(message) == 0 {
|
||||
return fmt.Errorf("message cannot be empty")
|
||||
}
|
||||
if len(message) > 500 {
|
||||
return fmt.Errorf("message too long (max 500 characters)")
|
||||
}
|
||||
|
||||
mb.mu.Lock()
|
||||
defer mb.mu.Unlock()
|
||||
|
||||
file, err := os.OpenFile(mb.filePath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.WriteString(fmt.Sprintf("[%s] %s: %s\n", time.Now().Format("2006-01-02 15:04:05"), username, message))
|
||||
return err
|
||||
}
|
||||
|
||||
func (mb *MessageBoard) GetMessages() ([]string, error) {
|
||||
mb.mu.Lock()
|
||||
defer mb.mu.Unlock()
|
||||
|
||||
content, err := os.ReadFile(mb.filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||
var messages []string
|
||||
for scanner.Scan() {
|
||||
messages = append(messages, scanner.Text())
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
}
|
18
internal/prompt/prompt.go
Normal file
18
internal/prompt/prompt.go
Normal file
@ -0,0 +1,18 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gbbs/internal/config"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ReadWelcomeScreen(cfg *config.Config) (string, error) {
|
||||
content, err := os.ReadFile(cfg.WelcomeScreenPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Sprintf("Welcome to GBBS!\n\nWelcome screen file not found: %s\n", cfg.WelcomeScreenPath), nil
|
||||
}
|
||||
return "", fmt.Errorf("error reading welcome screen: %v", err)
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
241
internal/ssh/ssh.go
Normal file
241
internal/ssh/ssh.go
Normal file
@ -0,0 +1,241 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/term"
|
||||
|
||||
"gbbs/internal/config"
|
||||
"gbbs/internal/messageboard"
|
||||
"gbbs/internal/prompt"
|
||||
"gbbs/internal/user"
|
||||
)
|
||||
|
||||
func generateSSHKey() (ssh.Signer, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ssh.NewSignerFromKey(key)
|
||||
}
|
||||
|
||||
func Serve(cfg *config.Config, userManager *user.Manager, messageBoard *messageboard.MessageBoard) error {
|
||||
signer, err := generateSSHKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate SSH key: %v", err)
|
||||
}
|
||||
|
||||
config := &ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
}
|
||||
config.AddHostKey(signer)
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.SSHPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("SSH server listening on port %d", cfg.SSHPort)
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Printf("Failed to accept incoming connection: %v", err)
|
||||
continue
|
||||
}
|
||||
go handleConnection(conn, config, cfg, userManager, messageBoard)
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn, config *ssh.ServerConfig, cfg *config.Config, userManager *user.Manager, messageBoard *messageboard.MessageBoard) {
|
||||
defer conn.Close()
|
||||
|
||||
sshConn, chans, reqs, err := ssh.NewServerConn(conn, config)
|
||||
if err != nil {
|
||||
log.Printf("Failed to handshake: %v", err)
|
||||
return
|
||||
}
|
||||
defer sshConn.Close()
|
||||
|
||||
log.Printf("New SSH connection from %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
|
||||
|
||||
go ssh.DiscardRequests(reqs)
|
||||
|
||||
for newChannel := range chans {
|
||||
if newChannel.ChannelType() != "session" {
|
||||
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||
continue
|
||||
}
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
log.Printf("Could not accept channel: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
go func(in <-chan *ssh.Request) {
|
||||
for req := range in {
|
||||
ok := false
|
||||
switch req.Type {
|
||||
case "shell":
|
||||
ok = true
|
||||
if len(req.Payload) > 0 {
|
||||
ok = false
|
||||
}
|
||||
case "pty-req":
|
||||
ok = true
|
||||
}
|
||||
req.Reply(ok, nil)
|
||||
}
|
||||
}(requests)
|
||||
|
||||
term := term.NewTerminal(channel, "> ")
|
||||
go handleSSHSession(term, cfg, userManager, messageBoard)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSSHSession(term *term.Terminal, cfg *config.Config, userManager *user.Manager, messageBoard *messageboard.MessageBoard) {
|
||||
defer term.Write([]byte("Goodbye!\n"))
|
||||
|
||||
welcomeScreen, err := prompt.ReadWelcomeScreen(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "Error reading welcome screen: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
term.Write([]byte(welcomeScreen))
|
||||
term.Write([]byte("\n\n\n\n\n")) // new lines to make it look real nice yo
|
||||
|
||||
for {
|
||||
term.SetPrompt("\033[0;32mChoose (L)ogin or (R)egister: \033[0m")
|
||||
choice, err := term.ReadLine()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(term, "\033[0;31mError reading input: %v\033[0m\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(choice)) {
|
||||
case "l":
|
||||
username, err := login(term, userManager)
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "\033[0;31mLogin failed: %v\033[0m\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(term, "\n\033[1;32mLogin successful! Welcome, %s!\033[0m\n", username)
|
||||
time.Sleep(2 * time.Second)
|
||||
handleBBS(term, username, messageBoard)
|
||||
return
|
||||
case "r":
|
||||
username, err := register(term, userManager)
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "\033[0;31mRegistration failed: %v\033[0m\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(term, "\n\033[1;32mRegistration successful! Welcome, %s!\033[0m\n", username)
|
||||
time.Sleep(2 * time.Second)
|
||||
handleBBS(term, username, messageBoard)
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(term, "\033[0;31mInvalid choice. Please enter 'L' or 'R'.\033[0m\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func login(term *term.Terminal, userManager *user.Manager) (string, error) {
|
||||
term.SetPrompt("Username: ")
|
||||
username, err := term.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
term.SetPrompt("Password: ")
|
||||
password, err := term.ReadPassword("Password: ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
authenticated, err := userManager.Authenticate(username, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !authenticated {
|
||||
return "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func register(term *term.Terminal, userManager *user.Manager) (string, error) {
|
||||
term.SetPrompt("Choose a username: ")
|
||||
username, err := term.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
term.SetPrompt("Choose a password: ")
|
||||
password, err := term.ReadPassword("Choose a password: ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = userManager.CreateUser(username, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func handleBBS(term *term.Terminal, username string, messageBoard *messageboard.MessageBoard) {
|
||||
for {
|
||||
term.Write([]byte("\n\033[0;36mBBS Menu:\033[0m\n"))
|
||||
term.Write([]byte("1. Read messages\n"))
|
||||
term.Write([]byte("2. Post message\n"))
|
||||
term.Write([]byte("3. Logout\n"))
|
||||
term.SetPrompt("Choice: ")
|
||||
|
||||
choice, err := term.ReadLine()
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "\033[0;31mError reading input: %v\033[0m\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(choice) {
|
||||
case "1":
|
||||
messages, err := messageBoard.GetMessages()
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "\033[0;31mError reading messages: %v\033[0m\n", err)
|
||||
} else {
|
||||
for _, msg := range messages {
|
||||
fmt.Fprintf(term, "%s\n", msg)
|
||||
}
|
||||
}
|
||||
case "2":
|
||||
term.SetPrompt("Enter your message: ")
|
||||
message, err := term.ReadLine()
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "\033[0;31mError reading message: %v\033[0m\n", err)
|
||||
continue
|
||||
}
|
||||
err = messageBoard.PostMessage(username, message)
|
||||
if err != nil {
|
||||
fmt.Fprintf(term, "\033[0;31mError posting message: %v\033[0m\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(term, "\033[0;32mMessage posted successfully!\033[0m\n")
|
||||
}
|
||||
case "3":
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(term, "\033[0;31mInvalid choice. Please try again.\033[0m\n")
|
||||
}
|
||||
}
|
||||
}
|
170
internal/telnet/telnet.go
Normal file
170
internal/telnet/telnet.go
Normal file
@ -0,0 +1,170 @@
|
||||
package telnet
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"gbbs/internal/config"
|
||||
"gbbs/internal/messageboard"
|
||||
"gbbs/internal/prompt"
|
||||
"gbbs/internal/user"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Serve(cfg *config.Config, userManager *user.Manager, messageBoard *messageboard.MessageBoard) error {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", cfg.TelnetPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
go handleConnection(conn, cfg, userManager, messageBoard)
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn, cfg *config.Config, userManager *user.Manager, messageBoard *messageboard.MessageBoard) {
|
||||
defer conn.Close()
|
||||
|
||||
writer := bufio.NewWriter(conn)
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
welcomeScreen, err := prompt.ReadWelcomeScreen(cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "Error reading welcome screen: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprint(writer, welcomeScreen)
|
||||
writer.Flush()
|
||||
|
||||
for {
|
||||
fmt.Fprintf(writer, "\n\033[0;31mPlease choose an option:\033[0m\n")
|
||||
fmt.Fprintf(writer, "\033[1;32mL\033[0m - Login\n")
|
||||
fmt.Fprintf(writer, "\033[1;32mR\033[0m - Register\n\n")
|
||||
fmt.Fprintf(writer, "\033[0;32mYour choice: \033[0m")
|
||||
writer.Flush()
|
||||
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
|
||||
if strings.EqualFold(choice, "L") {
|
||||
username, err := login(reader, writer, userManager)
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "\033[0;31mLogin failed: %v\033[0m\n", err)
|
||||
writer.Flush()
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(writer, "\n\033[1;32mLogin successful! Welcome, %s!\033[0m\n", username)
|
||||
writer.Flush()
|
||||
time.Sleep(2 * time.Second) // Pause for 2 seconds to show the message
|
||||
handleBBS(username, reader, writer, messageBoard)
|
||||
return
|
||||
} else if strings.EqualFold(choice, "R") {
|
||||
username, err := register(reader, writer, userManager)
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "\033[0;31mRegistration failed: %v\033[0m\n", err)
|
||||
writer.Flush()
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(writer, "\n\033[1;32mRegistration successful! Welcome, %s!\033[0m\n", username)
|
||||
writer.Flush()
|
||||
time.Sleep(2 * time.Second) // Pause for 2 seconds to show the message
|
||||
handleBBS(username, reader, writer, messageBoard)
|
||||
return
|
||||
} else {
|
||||
fmt.Fprintf(writer, "\033[0;31mInvalid choice. Please try again.\033[0m\n")
|
||||
writer.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func login(reader *bufio.Reader, writer *bufio.Writer, userManager *user.Manager) (string, error) {
|
||||
fmt.Fprintf(writer, "Username: ")
|
||||
writer.Flush()
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Fprintf(writer, "Password: ")
|
||||
writer.Flush()
|
||||
password, _ := reader.ReadString('\n')
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
authenticated, err := userManager.Authenticate(username, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !authenticated {
|
||||
return "", fmt.Errorf("invalid username or password")
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func register(reader *bufio.Reader, writer *bufio.Writer, userManager *user.Manager) (string, error) {
|
||||
fmt.Fprintf(writer, "Choose a username: ")
|
||||
writer.Flush()
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Fprintf(writer, "Choose a password: ")
|
||||
writer.Flush()
|
||||
password, _ := reader.ReadString('\n')
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
err := userManager.CreateUser(username, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func handleBBS(username string, reader *bufio.Reader, writer *bufio.Writer, messageBoard *messageboard.MessageBoard) {
|
||||
for {
|
||||
fmt.Fprintf(writer, "\n\033[0;36mBBS Menu:\033[0m\n")
|
||||
fmt.Fprintf(writer, "1. Read messages\n")
|
||||
fmt.Fprintf(writer, "2. Post message\n")
|
||||
fmt.Fprintf(writer, "3. Logout\n")
|
||||
fmt.Fprintf(writer, "Choice: ")
|
||||
writer.Flush()
|
||||
|
||||
choice, _ := reader.ReadString('\n')
|
||||
choice = strings.TrimSpace(choice)
|
||||
|
||||
switch choice {
|
||||
case "1":
|
||||
messages, err := messageBoard.GetMessages()
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "\033[0;31mError reading messages: %v\033[0m\n", err)
|
||||
} else {
|
||||
for _, msg := range messages {
|
||||
fmt.Fprintf(writer, "%s\n", msg)
|
||||
}
|
||||
}
|
||||
case "2":
|
||||
fmt.Fprintf(writer, "Enter your message: ")
|
||||
writer.Flush()
|
||||
message, _ := reader.ReadString('\n')
|
||||
message = strings.TrimSpace(message)
|
||||
err := messageBoard.PostMessage(username, message)
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "\033[0;31mError posting message: %v\033[0m\n", err)
|
||||
} else {
|
||||
fmt.Fprintf(writer, "\033[0;32mMessage posted successfully!\033[0m\n")
|
||||
}
|
||||
case "3":
|
||||
fmt.Fprintf(writer, "\033[0;33mGoodbye!\033[0m\n")
|
||||
writer.Flush()
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(writer, "\033[0;31mInvalid choice. Please try again.\033[0m\n")
|
||||
}
|
||||
writer.Flush()
|
||||
}
|
||||
}
|
95
internal/user/user.go
Normal file
95
internal/user/user.go
Normal file
@ -0,0 +1,95 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidUsername = errors.New("invalid username")
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
ErrUserExists = errors.New("user already exists")
|
||||
)
|
||||
|
||||
func NewManager(dbPath string) (*Manager, error) {
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE,
|
||||
password TEXT
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Manager{db: db}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
return m.db.Close()
|
||||
}
|
||||
|
||||
func (m *Manager) Authenticate(username, password string) (bool, error) {
|
||||
var storedPassword string
|
||||
err := m.db.QueryRow("SELECT password FROM users WHERE username = ?", username).Scan(&storedPassword)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(storedPassword), []byte(password))
|
||||
return err == nil, nil
|
||||
}
|
||||
|
||||
func (m *Manager) CreateUser(username, password string) error {
|
||||
if err := validateUsername(username); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validatePassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = m.db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, string(hashedPassword))
|
||||
if err != nil {
|
||||
if err.Error() == "UNIQUE constraint failed: users.username" {
|
||||
return ErrUserExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateUsername(username string) error {
|
||||
if len(username) < 3 || len(username) > 20 {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
return nil
|
||||
}
|
99
internal/web/web.go
Normal file
99
internal/web/web.go
Normal file
@ -0,0 +1,99 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gbbs/internal/messageboard"
|
||||
"gbbs/internal/user"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Serve(port int, webRoot string, userManager *user.Manager, messageBoard *messageboard.MessageBoard) error {
|
||||
http.Handle("/", http.FileServer(http.Dir(webRoot)))
|
||||
http.HandleFunc("/api/login", loginHandler(userManager))
|
||||
http.HandleFunc("/api/register", registerHandler(userManager))
|
||||
http.HandleFunc("/api/messages", messagesHandler(messageBoard))
|
||||
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
|
||||
}
|
||||
|
||||
func loginHandler(userManager *user.Manager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var creds struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := userManager.Authenticate(creds.Username, creds.Password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Login successful"})
|
||||
}
|
||||
}
|
||||
|
||||
func registerHandler(userManager *user.Manager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var creds struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := userManager.CreateUser(creds.Username, creds.Password); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "User created successfully"})
|
||||
}
|
||||
}
|
||||
|
||||
func messagesHandler(messageBoard *messageboard.MessageBoard) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
messages, err := messageBoard.GetMessages()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(messages)
|
||||
case http.MethodPost:
|
||||
var msg struct {
|
||||
Username string `json:"username"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := messageBoard.PostMessage(msg.Username, msg.Message); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Message posted successfully"})
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
10
welcome.ans
Normal file
10
welcome.ans
Normal file
@ -0,0 +1,10 @@
|
||||
Welcome to
|
||||
░▒▓██████▓▒░░▒▓███████▓▒░░▒▓███████▓▒░ ░▒▓███████▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
|
||||
░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
|
||||
░▒▓█▓▒▒▓███▓▒░▒▓███████▓▒░░▒▓███████▓▒░ ░▒▓██████▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░
|
||||
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░
|
||||
░▒▓██████▓▒░░▒▓███████▓▒░░▒▓███████▓▒░░▒▓███████▓▒░
|
||||
|
||||
ver 0.7 alpha
|
Loading…
Reference in New Issue
Block a user