Compare commits
4 Commits
3f7205d73e
...
v1.1.1
Author | SHA1 | Date | |
---|---|---|---|
f9ec811b10 | |||
d25ee09155 | |||
e3c570349c | |||
27da845b11 |
24
LICENSE
24
LICENSE
@ -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.
|
||||||
|
60
db/db.go
60
db/db.go
@ -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] = ®istrationAttempt{
|
||||||
|
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
58
main.go
@ -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()
|
||||||
|
Reference in New Issue
Block a user