4 Commits

Author SHA1 Message Date
f9ec811b10 Update License 2025-01-20 01:16:42 +01:00
d25ee09155 Add system messages to chat 2025-01-20 00:03:15 +01:00
e3c570349c Merge pull request 'feature/db' (#2) from feature/db into master
Reviewed-on: #2
2025-01-19 21:07:47 +00:00
27da845b11 Rate limit on registration 2025-01-19 22:06:41 +01:00
3 changed files with 128 additions and 14 deletions

24
LICENSE
View File

@ -1,11 +1,21 @@
“Commons Clause” License Condition v1.0 MIT License
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. Copyright (c) 2025 bdnugget
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software. Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Software: GoonServer (for Goonscape) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
License: Commons Clause v1.0 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
Licensor: bdnugget FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -5,6 +5,8 @@ import (
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"sync"
"time" "time"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
@ -17,6 +19,64 @@ var (
ErrInvalidCredentials = errors.New("invalid username or password") ErrInvalidCredentials = errors.New("invalid username or password")
) )
const (
maxRegistrationsPerIP = 3 // Maximum registrations allowed per IP
registrationWindow = 24 * time.Hour // Time window for rate limiting
)
type registrationAttempt struct {
count int
firstTry time.Time
}
var (
registrationAttempts = make(map[string]*registrationAttempt)
rateLimitMutex sync.RWMutex
)
func CleanupOldAttempts() {
rateLimitMutex.Lock()
defer rateLimitMutex.Unlock()
now := time.Now()
for ip, attempt := range registrationAttempts {
if now.Sub(attempt.firstTry) > registrationWindow {
delete(registrationAttempts, ip)
}
}
}
func CheckRegistrationLimit(ip string) error {
rateLimitMutex.Lock()
defer rateLimitMutex.Unlock()
now := time.Now()
attempt, exists := registrationAttempts[ip]
if !exists {
registrationAttempts[ip] = &registrationAttempt{
count: 1,
firstTry: now,
}
return nil
}
// Reset if window has passed
if now.Sub(attempt.firstTry) > registrationWindow {
attempt.count = 1
attempt.firstTry = now
return nil
}
if attempt.count >= maxRegistrationsPerIP {
return fmt.Errorf("registration limit reached for this IP. Please try again in %v",
registrationWindow-now.Sub(attempt.firstTry))
}
attempt.count++
return nil
}
func InitDB(dbPath string) error { func InitDB(dbPath string) error {
var err error var err error
db, err = sql.Open("sqlite3", dbPath) db, err = sql.Open("sqlite3", dbPath)

58
main.go
View File

@ -30,13 +30,12 @@ type Player struct {
} }
var ( var (
players = make(map[int]*Player) players = make(map[int]*Player)
actionQueue = make(map[int][]*pb.Action) // Queue to store actions for each player actionQueue = make(map[int][]*pb.Action) // Queue to store actions for each player
playerConns = make(map[int]net.Conn) // Map to store player connections playerConns = make(map[int]net.Conn) // Map to store player connections
mu sync.RWMutex // Add mutex for protecting shared maps mu sync.RWMutex // Add mutex for protecting shared maps
chatHistory = make([]*pb.ChatMessage, 0, 100) chatHistory = make([]*pb.ChatMessage, 0, 100)
chatMutex sync.RWMutex chatMutex sync.RWMutex
nextPlayerID = 1 // Assuming player IDs start from 1
) )
func main() { func main() {
@ -55,6 +54,15 @@ func main() {
ticker := time.NewTicker(tickRate) ticker := time.NewTicker(tickRate)
defer ticker.Stop() defer ticker.Stop()
// Start registration attempt cleanup goroutine
go func() {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
db.CleanupOldAttempts()
}
}()
// Handle incoming connections in a separate goroutine // Handle incoming connections in a separate goroutine
go func() { go func() {
for { for {
@ -76,6 +84,14 @@ func main() {
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
// Get client IP
remoteAddr := conn.RemoteAddr().String()
ip, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
log.Printf("Failed to parse remote address: %v", err)
return
}
// Read initial message for player ID // Read initial message for player ID
reader := bufio.NewReader(conn) reader := bufio.NewReader(conn)
@ -130,6 +146,14 @@ func handleConnection(conn net.Conn) {
switch action.Type { switch action.Type {
case pb.Action_REGISTER: case pb.Action_REGISTER:
if err := db.CheckRegistrationLimit(ip); err != nil {
response := &pb.ServerMessage{
AuthSuccess: false,
ErrorMessage: err.Error(),
}
writeMessage(conn, response)
return
}
playerID, authErr = db.RegisterPlayer(action.Username, action.Password) playerID, authErr = db.RegisterPlayer(action.Username, action.Password)
case pb.Action_LOGIN: case pb.Action_LOGIN:
playerID, authErr = db.AuthenticatePlayer(action.Username, action.Password) playerID, authErr = db.AuthenticatePlayer(action.Username, action.Password)
@ -201,11 +225,14 @@ func handleConnection(conn net.Conn) {
ProtocolVersion: protoVersion, ProtocolVersion: protoVersion,
} }
addSystemMessage(fmt.Sprintf("%s connected", username))
// Ensure player state is saved on any kind of disconnect // Ensure player state is saved on any kind of disconnect
defer func() { defer func() {
if err := db.SavePlayerState(playerID, player.X, player.Y); err != nil { if err := db.SavePlayerState(playerID, player.X, player.Y); err != nil {
log.Printf("Error saving state for player %d: %v", playerID, err) log.Printf("Error saving state for player %d: %v", playerID, err)
} }
addSystemMessage(fmt.Sprintf("%s disconnected", player.Username))
mu.Lock() mu.Lock()
delete(players, playerID) delete(players, playerID)
delete(playerConns, playerID) delete(playerConns, playerID)
@ -284,6 +311,23 @@ func addChatMessage(playerID int32, content string) {
chatHistory = append(chatHistory, msg) chatHistory = append(chatHistory, msg)
} }
func addSystemMessage(content string) {
chatMutex.Lock()
defer chatMutex.Unlock()
msg := &pb.ChatMessage{
PlayerId: 0, // System messages use ID 0
Username: "System",
Content: content,
Timestamp: time.Now().UnixNano(),
}
if len(chatHistory) >= 100 {
chatHistory = chatHistory[1:]
}
chatHistory = append(chatHistory, msg)
}
func processActions() { func processActions() {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()