Added all of the existing code
This commit is contained in:
330
FREEZING_ISSUE_FIXES.md
Normal file
330
FREEZING_ISSUE_FIXES.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# TechIRCd Freezing Issue - Fixes Applied
|
||||
|
||||
## Issue Description
|
||||
TechIRCd was experiencing freezing/hanging issues where the server would become unresponsive after running for a while, preventing new user connections and causing existing users to disconnect.
|
||||
|
||||
## Root Causes Identified
|
||||
|
||||
### 1. Resource Leaks in Client Handling
|
||||
- **Problem**: Disconnected clients weren't being properly cleaned up, leading to memory leaks and resource exhaustion
|
||||
- **Symptoms**: Server gradually becoming slower and eventually unresponsive
|
||||
|
||||
### 2. Goroutine Leaks
|
||||
- **Problem**: Client handler goroutines weren't exiting properly when connections were broken
|
||||
- **Symptoms**: Increasing number of goroutines over time, eventually exhausting system resources
|
||||
|
||||
### 3. Lock Contention in Ping/Health Checks
|
||||
- **Problem**: Server-wide ping and health checks were holding locks too long and running synchronously
|
||||
- **Symptoms**: Server becoming unresponsive during health checks, client operations timing out
|
||||
|
||||
### 4. Inefficient Connection State Management
|
||||
- **Problem**: Inconsistent tracking of client connection state, leading to operations on dead connections
|
||||
- **Symptoms**: Hanging writes, blocked goroutines, server freezing
|
||||
|
||||
### 5. Deadlocked Shutdown Process
|
||||
- **Problem**: Shutdown process could deadlock when trying to notify clients while holding locks
|
||||
- **Symptoms**: Ctrl+C not working, server becoming completely unresponsive, requiring SIGKILL
|
||||
|
||||
## Fixes Applied
|
||||
|
||||
### 1. Enhanced Client Cleanup (`client.go`)
|
||||
|
||||
#### Before:
|
||||
```go
|
||||
func (c *Client) cleanup() {
|
||||
// Basic cleanup with potential blocking operations
|
||||
if c.conn != nil {
|
||||
c.conn.Close() // Could hang
|
||||
}
|
||||
// Synchronous channel cleanup
|
||||
// Synchronous server removal
|
||||
}
|
||||
```
|
||||
|
||||
#### After:
|
||||
```go
|
||||
func (c *Client) cleanup() {
|
||||
// Non-blocking cleanup with timeouts
|
||||
if c.conn != nil {
|
||||
c.conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
c.conn.Close()
|
||||
}
|
||||
// Asynchronous channel cleanup in goroutine
|
||||
// Asynchronous server removal in goroutine
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Prevents cleanup operations from blocking other clients
|
||||
- Uses timeouts to force close hanging connections
|
||||
- Prevents deadlocks during shutdown
|
||||
|
||||
### 2. Improved Connection Handling
|
||||
|
||||
#### Connection Timeout Management:
|
||||
```go
|
||||
func (c *Client) handleMessageRead(...) bool {
|
||||
// Set read deadline with timeout to prevent hanging
|
||||
readTimeout := 30 * time.Second
|
||||
if c.IsRegistered() {
|
||||
readTimeout = 5 * time.Minute
|
||||
}
|
||||
|
||||
// Use goroutine with timeout for non-blocking reads
|
||||
scanChan := make(chan bool, 1)
|
||||
go func() {
|
||||
// Scanner in separate goroutine
|
||||
scanChan <- scanner.Scan()
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-scanChan:
|
||||
// Process result
|
||||
case <-time.After(readTimeout):
|
||||
// Handle timeout
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Prevents infinite blocking on read operations
|
||||
- Detects and handles dead connections quickly
|
||||
- Provides graceful timeout handling
|
||||
|
||||
### 3. Non-blocking Ping and Health Checks (`server.go`)
|
||||
|
||||
#### Before:
|
||||
```go
|
||||
func (s *Server) pingRoutine() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.performPingCheck() // Blocking operation
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After:
|
||||
```go
|
||||
func (s *Server) pingRoutine() {
|
||||
ticker := time.NewTicker(60 * time.Second) // Less frequent
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
go s.performPingCheck() // Non-blocking
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Enhanced Ping Check:
|
||||
```go
|
||||
func (s *Server) performPingCheck() {
|
||||
// Get snapshot of client IDs without holding lock
|
||||
s.mu.RLock()
|
||||
clientIDs := make([]string, 0, len(s.clients))
|
||||
for clientID := range s.clients {
|
||||
clientIDs = append(clientIDs, clientID)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
// Process clients individually to prevent blocking
|
||||
for _, clientID := range clientIDs {
|
||||
// Non-blocking ping sending
|
||||
go func(id string) {
|
||||
// Send ping in separate goroutine
|
||||
}(clientID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Eliminates blocking during server-wide operations
|
||||
- Reduces lock contention
|
||||
- Prevents cascade failures
|
||||
|
||||
### 4. Batched Health Checks
|
||||
|
||||
#### Before:
|
||||
```go
|
||||
func (s *Server) performHealthCheck() {
|
||||
// Hold lock for entire operation
|
||||
s.mu.RLock()
|
||||
clients := make([]*Client, 0, len(s.clients))
|
||||
// ... process all clients synchronously
|
||||
s.mu.RUnlock()
|
||||
}
|
||||
```
|
||||
|
||||
#### After:
|
||||
```go
|
||||
func (s *Server) performHealthCheck() {
|
||||
// Process clients in batches of 50
|
||||
batchSize := 50
|
||||
for i := 0; i < len(clientIDs); i += batchSize {
|
||||
batch := clientIDs[i:end]
|
||||
for _, clientID := range batch {
|
||||
// Process each client individually
|
||||
}
|
||||
// Small delay between batches
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Prevents overwhelming the system during health checks
|
||||
- Allows other operations to proceed between batches
|
||||
- Reduces memory usage during large client counts
|
||||
|
||||
### 5. Enhanced Error Recovery
|
||||
|
||||
#### Panic Recovery:
|
||||
```go
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic in client handler for %s: %v", c.getClientInfo(), r)
|
||||
}
|
||||
c.cleanup()
|
||||
}()
|
||||
```
|
||||
|
||||
#### Graceful Disconnection:
|
||||
```go
|
||||
func (c *Client) ForceDisconnect(reason string) {
|
||||
log.Printf("Force disconnecting client %s: %s", c.getClientInfo(), reason)
|
||||
|
||||
c.mu.Lock()
|
||||
c.disconnected = true
|
||||
c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
c.SendMessage(fmt.Sprintf("ERROR :%s", reason))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Robust Shutdown Process (`server.go` & `main.go`)
|
||||
|
||||
#### Before:
|
||||
```go
|
||||
func (s *Server) Shutdown() {
|
||||
// Could deadlock holding locks
|
||||
s.mu.RLock()
|
||||
for _, client := range s.clients {
|
||||
client.SendMessage("ERROR :Server shutting down")
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
close(s.shutdown)
|
||||
}
|
||||
```
|
||||
|
||||
#### After:
|
||||
```go
|
||||
func (s *Server) Shutdown() {
|
||||
// Non-blocking shutdown with timeout protection
|
||||
|
||||
// Close listeners immediately
|
||||
go func() { /* close listeners */ }()
|
||||
|
||||
// Signal shutdown first
|
||||
close(s.shutdown)
|
||||
|
||||
// Disconnect clients in batches asynchronously
|
||||
go func() {
|
||||
// Process clients in batches of 10
|
||||
// Each client disconnection in separate goroutine
|
||||
// Timeout protection for each operation
|
||||
}()
|
||||
|
||||
// Force shutdown after reasonable timeout
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
```
|
||||
|
||||
#### Signal Handling with Force Option:
|
||||
```go
|
||||
// Double Ctrl+C for immediate shutdown
|
||||
if shutdownInProgress {
|
||||
log.Println("Forcing immediate shutdown...")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Timeout protection for graceful shutdown
|
||||
select {
|
||||
case <-shutdownComplete:
|
||||
log.Println("Graceful shutdown completed")
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Println("Shutdown timeout, forcing exit...")
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Prevents deadlocks during shutdown
|
||||
- Allows double Ctrl+C for immediate force shutdown
|
||||
- Timeout protection prevents hanging shutdown
|
||||
- Asynchronous operations prevent blocking
|
||||
|
||||
## Configuration Optimizations
|
||||
|
||||
### Timing Adjustments:
|
||||
- **Ping Interval**: Increased from 30s to 60s to reduce overhead
|
||||
- **Health Check Interval**: Increased from 60s to 5 minutes
|
||||
- **Read Timeouts**: More conservative timeouts for better stability
|
||||
- **Registration Timeout**: Better enforcement to prevent hanging registrations
|
||||
|
||||
### Resource Limits:
|
||||
- **Batch Processing**: Health checks limited to 50 clients per batch
|
||||
- **Connection Limits**: Better enforcement of max client limits
|
||||
- **Memory Management**: Proactive cleanup of disconnected clients
|
||||
|
||||
## Expected Results
|
||||
|
||||
1. **Stability**: Server should remain responsive under normal load
|
||||
2. **Resource Usage**: More predictable memory and goroutine usage
|
||||
3. **Connection Handling**: Faster detection and cleanup of dead connections
|
||||
4. **Performance**: Reduced lock contention and blocking operations
|
||||
5. **Monitoring**: Better logging and health monitoring
|
||||
|
||||
## Monitoring
|
||||
|
||||
The server now provides better logging for:
|
||||
- Client connection/disconnection events
|
||||
- Health check results and statistics
|
||||
- Resource usage patterns
|
||||
- Error conditions and recovery actions
|
||||
|
||||
## Testing
|
||||
|
||||
I've created test scripts to verify the fixes:
|
||||
|
||||
### 1. Shutdown Test (`test_shutdown.sh`)
|
||||
- Tests graceful shutdown behavior
|
||||
- Verifies server responds to SIGTERM
|
||||
- Confirms shutdown completes within reasonable time
|
||||
|
||||
### 2. Stress Test (`test_stress.sh`)
|
||||
- Simulates conditions that previously caused freezing
|
||||
- Creates multiple stable and unstable connections
|
||||
- Tests rapid connection/disconnection patterns
|
||||
- Monitors server responsiveness during stress
|
||||
- Verifies shutdown works after stress conditions
|
||||
|
||||
### Usage:
|
||||
```bash
|
||||
# Test shutdown behavior
|
||||
./test_shutdown.sh
|
||||
|
||||
# Test stability under stress
|
||||
./test_stress.sh
|
||||
```
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Metrics Endpoint**: Add HTTP endpoint for real-time metrics
|
||||
2. **Connection Pooling**: Implement connection pooling for better resource management
|
||||
3. **Circuit Breakers**: Add circuit breakers for failing operations
|
||||
4. **Rate Limiting**: Enhanced rate limiting per IP/user
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 ComputerTech312
|
||||
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
117
Makefile
Normal file
117
Makefile
Normal file
@@ -0,0 +1,117 @@
|
||||
.PHONY: build run clean test lint fmt help
|
||||
|
||||
# Default target
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# Variables
|
||||
BINARY_NAME=techircd
|
||||
CMD_PATH=.
|
||||
VERSION?=$(shell git describe --tags --always --dirty)
|
||||
LDFLAGS=-ldflags "-X main.version=$(VERSION)"
|
||||
|
||||
## Build Commands
|
||||
|
||||
# Build the IRC server
|
||||
build: ## Build the binary
|
||||
go build $(LDFLAGS) -o $(BINARY_NAME) $(CMD_PATH)
|
||||
|
||||
# Build for different platforms
|
||||
build-all: ## Build for multiple platforms
|
||||
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-linux-amd64 $(CMD_PATH)
|
||||
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-windows-amd64.exe $(CMD_PATH)
|
||||
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-darwin-amd64 $(CMD_PATH)
|
||||
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BINARY_NAME)-darwin-arm64 $(CMD_PATH)
|
||||
|
||||
## Development Commands
|
||||
|
||||
# Run the server
|
||||
run: build ## Build and run the server
|
||||
./$(BINARY_NAME)
|
||||
|
||||
# Run with custom config
|
||||
run-config: build ## Run with custom config file
|
||||
./$(BINARY_NAME) -config configs/config.dev.json
|
||||
|
||||
# Install dependencies
|
||||
deps: ## Download and install dependencies
|
||||
go mod download
|
||||
go mod tidy
|
||||
|
||||
# Format code
|
||||
fmt: ## Format Go code
|
||||
go fmt ./...
|
||||
goimports -w -local github.com/ComputerTech312/TechIRCd .
|
||||
|
||||
# Lint code
|
||||
lint: ## Run linters
|
||||
golangci-lint run
|
||||
|
||||
# Fix linting issues
|
||||
lint-fix: ## Fix auto-fixable linting issues
|
||||
golangci-lint run --fix
|
||||
|
||||
## Testing Commands
|
||||
|
||||
# Run all tests
|
||||
test: ## Run all tests
|
||||
go test -v -race ./...
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage: ## Run tests with coverage report
|
||||
go test -v -race -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
|
||||
# Run benchmarks
|
||||
benchmark: ## Run benchmark tests
|
||||
go test -bench=. -benchmem ./...
|
||||
|
||||
# Test with the test client
|
||||
test-client: build ## Test with the simple client
|
||||
go run tests/test_client.go
|
||||
|
||||
## Release Commands
|
||||
|
||||
# Create a release build
|
||||
release: clean ## Create optimized release build
|
||||
CGO_ENABLED=0 go build $(LDFLAGS) -a -installsuffix cgo -o $(BINARY_NAME) $(CMD_PATH)
|
||||
|
||||
# Tag a new version
|
||||
tag: ## Tag a new version (usage: make tag VERSION=v1.0.1)
|
||||
git tag -a $(VERSION) -m "Release $(VERSION)"
|
||||
git push origin $(VERSION)
|
||||
|
||||
## Utility Commands
|
||||
|
||||
# Clean build artifacts
|
||||
clean: ## Clean build artifacts
|
||||
rm -f $(BINARY_NAME)*
|
||||
rm -f coverage.out coverage.html
|
||||
|
||||
# Show git status and recent commits
|
||||
status: ## Show git status and recent commits
|
||||
@echo "Git Status:"
|
||||
@git status --short
|
||||
@echo "\nRecent Commits:"
|
||||
@git log --oneline -10
|
||||
|
||||
# Install development tools
|
||||
install-tools: ## Install development tools
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go install golang.org/x/tools/cmd/goimports@latest
|
||||
|
||||
# Generate documentation
|
||||
docs: ## Generate documentation
|
||||
go doc -all > docs/API.md
|
||||
|
||||
# Show help
|
||||
help: ## Show this help message
|
||||
@echo 'Usage:'
|
||||
@echo ' make <target>'
|
||||
@echo ''
|
||||
@echo 'Targets:'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
go fmt ./...
|
||||
|
||||
# Run with race detection
|
||||
run-race: build
|
||||
go run -race *.go
|
||||
217
README.md
Normal file
217
README.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# TechIRCd
|
||||
|
||||
A modern, high-performance IRC server written in Go with comprehensive RFC compliance and advanced features.
|
||||
|
||||
## Features
|
||||
|
||||
### 🚀 **Core IRC Protocol**
|
||||
- Full RFC 2812 compliance (Internet Relay Chat: Client Protocol)
|
||||
- User registration and authentication
|
||||
- Channel management with comprehensive modes
|
||||
- Private messaging and notices
|
||||
- **Ultra-flexible WHOIS system** with granular privacy controls
|
||||
- WHO and NAMES commands
|
||||
- Ping/Pong keepalive mechanism
|
||||
|
||||
### 🔍 **Revolutionary WHOIS System**
|
||||
- **Granular Privacy Controls**: Configure exactly what information is visible to everyone, operators, or only the user themselves
|
||||
- **User Modes Visibility**: Control who can see user modes (+i, +w, +s, etc.)
|
||||
- **SSL Status Display**: Show secure connection status
|
||||
- **Idle Time & Signon Time**: Configurable time information
|
||||
- **Real Host vs Masked Host**: Smart hostname display based on permissions
|
||||
- **Channel Privacy**: Hide secret/private channels with fine-grained control
|
||||
- **Operator Information**: Show operator class and privileges
|
||||
- **Services Integration**: Account name display for services
|
||||
- **Client Information**: Show IRC client details
|
||||
- **Custom Fields**: Add your own WHOIS fields
|
||||
- See [WHOIS Configuration Guide](docs/WHOIS_CONFIGURATION.md) for details
|
||||
|
||||
### 👑 **Advanced Channel Management**
|
||||
- **Operator Hierarchy**: Owners (~), Operators (@), Half-ops (%), Voice (+)
|
||||
- **Channel Modes**:
|
||||
- `+m` (moderated) - Only voiced users can speak
|
||||
- `+n` (no external messages)
|
||||
- `+t` (topic protection)
|
||||
- `+i` (invite only)
|
||||
- `+s` (secret channel)
|
||||
- `+p` (private channel)
|
||||
- `+k` (channel key/password)
|
||||
- `+l` (user limit)
|
||||
- `+b` (ban list)
|
||||
- **Extended Ban System**: Support for quiet mode (`~q:mask`) and other extended ban types
|
||||
|
||||
### 🔐 **IRC Operator Features**
|
||||
- Comprehensive operator authentication system
|
||||
- **Hierarchical Operator Classes** with rank-based permissions and inheritance
|
||||
- **Completely Customizable Rank Names** (Gaming, Corporate, Fantasy themes, etc.)
|
||||
- **Server Notice Masks (SNOmasks)**:
|
||||
- `+c` (connection notices)
|
||||
- `+k` (kill notices)
|
||||
- +o (oper notices)
|
||||
- +x (ban/quiet notices)
|
||||
- +f (flood notices)
|
||||
- +n (nick change notices)
|
||||
- +s (server notices)
|
||||
- +d (debug notices)
|
||||
- Operator commands:
|
||||
- KILL - Disconnect users
|
||||
- GLOBALNOTICE - Send notices to all users
|
||||
- OPERWALL - Send messages to all operators
|
||||
- WALLOPS - Send wallops messages
|
||||
- REHASH - Reload configuration
|
||||
- TRACE - Network trace information
|
||||
- GODMODE - Toggle ultimate channel override powers
|
||||
- STEALTH - Toggle invisibility to regular users
|
||||
- Services/admin commands:
|
||||
- CHGHOST - Change user's hostname
|
||||
- SVSNICK - Services force nickname change
|
||||
- SVSMODE - Services force mode change
|
||||
- SAMODE - Services admin mode change
|
||||
- SANICK - Services admin nickname change
|
||||
- SAKICK - Services admin kick
|
||||
- SAPART - Services admin part
|
||||
- SAJOIN - Services admin join
|
||||
- GLINE - Global network ban
|
||||
- ZLINE - IP address ban
|
||||
- KLINE - Local server ban
|
||||
- SHUN - Services shun (silent ignore)
|
||||
|
||||
### 👤 **User Modes**
|
||||
- `+i` (invisible) - Hide from WHO listings
|
||||
- `+w` (wallops) - Receive wallops messages
|
||||
- `+s` (server notices) - Receive server notices (opers only)
|
||||
- `+o` (operator) - IRC operator status
|
||||
- `+x` (host masking) - Hide real hostname
|
||||
- `+B` (bot) - Mark as a bot
|
||||
- `+z` (SSL) - Connected via SSL/TLS
|
||||
- `+r` (registered) - Registered with services
|
||||
|
||||
### 🛡️ **Security & Stability**
|
||||
- Advanced flood protection with operator exemption
|
||||
- Connection timeout management
|
||||
- Input validation and sanitization
|
||||
- Panic recovery and error handling
|
||||
- Resource monitoring and health checks
|
||||
- Graceful shutdown capabilities
|
||||
|
||||
### ⚡ **Unique Features (Not Found in Other IRCds)**
|
||||
|
||||
#### 🌟 **God Mode (+G)**
|
||||
- **Ultimate Channel Override**: Join any channel regardless of bans, limits, keys, or invite-only mode
|
||||
- **Kick Immunity**: Cannot be kicked by any user
|
||||
- **Mode Override**: Set any channel mode without operator privileges
|
||||
- **Complete Bypass**: Ignore all channel restrictions and limitations
|
||||
|
||||
#### 👻 **Stealth Mode (+S)**
|
||||
- **User Invisibility**: Completely hidden from regular users in WHO, NAMES, and WHOIS
|
||||
- **Operator Visibility**: Other operators can still see stealth users
|
||||
- **Covert Monitoring**: Watch channels without being detected
|
||||
- **Security Operations**: Investigate issues invisibly
|
||||
|
||||
#### 🎨 **Customizable Rank Names**
|
||||
- **Gaming Themes**: Cadet → Sergeant → Lieutenant → Captain → General
|
||||
- **Corporate Themes**: Intern → Associate → Manager → Director → CEO
|
||||
- **Fantasy Themes**: Apprentice → Guardian → Knight → Lord → King
|
||||
- **Unlimited Creativity**: Create any rank system you can imagine
|
||||
|
||||
#### 🔍 **Revolutionary WHOIS**
|
||||
- **Granular Privacy**: 15+ configurable information types
|
||||
- **Three-Tier Permissions**: Everyone/Operators/Self visibility controls
|
||||
- **Custom Fields**: Add your own WHOIS information
|
||||
- **Complete Flexibility**: Control every aspect of user information display
|
||||
|
||||
### 📊 **Monitoring & Logging**
|
||||
- Real-time health monitoring
|
||||
- Memory usage tracking
|
||||
- Goroutine count monitoring
|
||||
- Performance metrics logging
|
||||
- Configurable log levels and rotation
|
||||
- Private messaging
|
||||
- WHO/WHOIS commands
|
||||
- Configurable server settings
|
||||
- Extensible architecture
|
||||
- Ping/Pong keep-alive mechanism
|
||||
- Graceful shutdown handling
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go 1.21 or higher
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
go mod tidy
|
||||
|
||||
# Build the server
|
||||
go run tools/build.go -build
|
||||
|
||||
# Or use the traditional method
|
||||
go build -o techircd
|
||||
```
|
||||
|
||||
## Build Options
|
||||
|
||||
The `tools/build.go` script provides several build options:
|
||||
|
||||
```bash
|
||||
# Build and run
|
||||
go run tools/build.go -run
|
||||
|
||||
# Run tests
|
||||
go run tools/build.go -test
|
||||
|
||||
# Format code
|
||||
go run tools/build.go -fmt
|
||||
|
||||
# Cross-platform builds
|
||||
go run tools/build.go -build-all
|
||||
|
||||
# Optimized release build
|
||||
go run tools/build.go -release
|
||||
|
||||
# Clean build artifacts
|
||||
go run tools/build.go -clean
|
||||
|
||||
# Show all options
|
||||
go run tools/build.go -help
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
./techircd
|
||||
```
|
||||
|
||||
The server will start on localhost:6667 by default. You can configure the host and port in `config.go`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.go` to customize:
|
||||
- Server host and port
|
||||
- Server name and description
|
||||
- Maximum connections
|
||||
- Ping timeout
|
||||
- Message of the Day (MOTD)
|
||||
|
||||
## Connecting
|
||||
|
||||
You can connect using any IRC client:
|
||||
```
|
||||
/server localhost 6667
|
||||
```
|
||||
|
||||
Or use the included test client:
|
||||
```bash
|
||||
go run test_client.go
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- `main.go` - Entry point and server initialization
|
||||
- `server.go` - Main IRC server implementation with concurrent connection handling
|
||||
- `client.go` - Client connection handling and state management
|
||||
- `channel.go` - Channel management with thread-safe operations
|
||||
- `commands.go` - IRC command implementations (NICK, USER, JOIN, PART, etc.)
|
||||
- `config.go` - Server configuration structure
|
||||
- `test_client.go` - Simple test client for development
|
||||
127
STRESS_TEST_README.md
Normal file
127
STRESS_TEST_README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# TechIRCd Stress Testing Tool
|
||||
|
||||
A comprehensive Python-based stress testing tool for TechIRCd IRC server.
|
||||
|
||||
## Features
|
||||
|
||||
- **Configurable Test Scenarios**: JSON-based configuration
|
||||
- **Multiple Test Types**: Connection tests, message flooding, command spam
|
||||
- **Gradual or Mass Operations**: Connect/disconnect clients gradually or all at once
|
||||
- **Real-time Statistics**: Connection counts, message statistics, error tracking
|
||||
- **Logging**: Detailed logging to file and console
|
||||
- **SSL Support**: Test both plain and SSL connections
|
||||
|
||||
## Requirements
|
||||
|
||||
```bash
|
||||
pip3 install asyncio # Usually included with Python 3.7+
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Run All Test Scenarios
|
||||
```bash
|
||||
python3 stress_test.py
|
||||
```
|
||||
|
||||
### Run Specific Scenario
|
||||
```bash
|
||||
python3 stress_test.py --scenario "Basic Connection Test"
|
||||
```
|
||||
|
||||
### Use Custom Configuration
|
||||
```bash
|
||||
python3 stress_test.py --config my_config.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `stress_config.json` to customize tests:
|
||||
|
||||
### Server Settings
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"host": "localhost", // IRC server hostname
|
||||
"port": 6667, // IRC server port
|
||||
"ssl": false, // Use SSL connection
|
||||
"ssl_port": 6697, // SSL port
|
||||
"nick_prefix": "StressBot", // Prefix for bot nicknames
|
||||
"auto_join_channels": ["#test"] // Channels to auto-join
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
Each scenario supports:
|
||||
- `client_count`: Number of concurrent clients
|
||||
- `duration`: How long to run the test (seconds)
|
||||
- `connect_gradually`: Connect clients one by one vs all at once
|
||||
- `connect_delay`: Delay between gradual connections
|
||||
- `disconnect_gradually`: Disconnect gradually vs all at once
|
||||
- `activities`: List of activities to perform during test
|
||||
|
||||
### Activity Types
|
||||
|
||||
1. **channel_flood**: Flood channels with messages
|
||||
2. **private_flood**: Send private messages between clients
|
||||
3. **join_part_spam**: Rapid JOIN/PART operations
|
||||
4. **nick_change_spam**: Rapid nickname changes
|
||||
5. **random_commands**: Send random IRC commands
|
||||
|
||||
## Pre-configured Test Scenarios
|
||||
|
||||
1. **Basic Connection Test** (10 clients, 30s)
|
||||
- Simple connection and registration test
|
||||
|
||||
2. **Mass Connection Stress** (50 clients, 60s)
|
||||
- Many simultaneous connections with channel flooding
|
||||
|
||||
3. **Gradual Connection Test** (30 clients, 45s)
|
||||
- Gradual connection buildup with private messaging
|
||||
|
||||
4. **Heavy Activity Test** (25 clients, 90s)
|
||||
- Multiple activity types running simultaneously
|
||||
|
||||
5. **Nick Change Spam** (15 clients, 30s)
|
||||
- Rapid nickname change testing
|
||||
|
||||
6. **Channel Chaos Test** (20 clients, 60s)
|
||||
- JOIN/PART spam with message flooding
|
||||
|
||||
7. **Maximum Load Test** (100 clients, 120s)
|
||||
- Push server to maximum capacity
|
||||
|
||||
## Monitoring
|
||||
|
||||
- **Real-time logs**: Monitor progress in console
|
||||
- **Log file**: Detailed logs saved to `stress_test.log`
|
||||
- **Statistics**: Connection counts, message statistics, error tracking
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
2025-09-07 12:00:00 - INFO - Starting scenario: Basic Connection Test
|
||||
2025-09-07 12:00:00 - INFO - Clients: 10, Duration: 30s
|
||||
2025-09-07 12:00:00 - INFO - Mass connecting 10 clients...
|
||||
2025-09-07 12:00:01 - INFO - Connected 10/10 clients
|
||||
2025-09-07 12:00:03 - INFO - Registered 10/10 clients
|
||||
2025-09-07 12:00:33 - INFO - Stats: 10 connected, 10 registered, 0 messages sent
|
||||
2025-09-07 12:00:35 - INFO - Scenario Basic Connection Test completed
|
||||
```
|
||||
|
||||
## Tips for Testing
|
||||
|
||||
1. **Start Small**: Begin with basic connection test
|
||||
2. **Monitor Resources**: Watch TechIRCd memory/CPU usage
|
||||
3. **Check Logs**: Review both stress test and TechIRCd logs
|
||||
4. **Gradual Increase**: Increase client counts gradually
|
||||
5. **Test Different Scenarios**: Each scenario tests different aspects
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Connection Refused**: Make sure TechIRCd is running
|
||||
- **SSL Errors**: Check SSL configuration in both TechIRCd and test config
|
||||
- **Memory Issues**: Reduce client count or test duration
|
||||
- **Python Errors**: Ensure Python 3.7+ with asyncio support
|
||||
836
channel.go
Normal file
836
channel.go
Normal file
@@ -0,0 +1,836 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
name string
|
||||
topic string
|
||||
topicBy string
|
||||
topicTime time.Time
|
||||
clients map[string]*Client
|
||||
operators map[string]*Client
|
||||
halfops map[string]*Client
|
||||
voices map[string]*Client
|
||||
owners map[string]*Client
|
||||
admins map[string]*Client
|
||||
modes map[rune]bool
|
||||
key string
|
||||
limit int
|
||||
banList []string
|
||||
quietList []string // Users who can join but not speak (+q)
|
||||
exceptList []string // Users exempt from bans (+e)
|
||||
inviteList []string // Users who can join invite-only channels (+I)
|
||||
created time.Time
|
||||
|
||||
// Advanced mode settings
|
||||
floodSettings string // Flood protection settings (e.g., "10:5")
|
||||
joinThrottle string // Join throttling settings (e.g., "3:10")
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewChannel(name string) *Channel {
|
||||
return &Channel{
|
||||
name: name,
|
||||
clients: make(map[string]*Client),
|
||||
operators: make(map[string]*Client),
|
||||
halfops: make(map[string]*Client),
|
||||
voices: make(map[string]*Client),
|
||||
owners: make(map[string]*Client),
|
||||
admins: make(map[string]*Client),
|
||||
modes: make(map[rune]bool),
|
||||
banList: make([]string, 0),
|
||||
quietList: make([]string, 0),
|
||||
exceptList: make([]string, 0),
|
||||
inviteList: make([]string, 0),
|
||||
created: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) Name() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.name
|
||||
}
|
||||
|
||||
func (ch *Channel) Topic() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.topic
|
||||
}
|
||||
|
||||
func (ch *Channel) TopicBy() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.topicBy
|
||||
}
|
||||
|
||||
func (ch *Channel) TopicTime() time.Time {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.topicTime
|
||||
}
|
||||
|
||||
func (ch *Channel) SetTopic(topic, by string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.topic = topic
|
||||
ch.topicBy = by
|
||||
ch.topicTime = time.Now()
|
||||
}
|
||||
|
||||
func (ch *Channel) AddClient(client *Client) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
|
||||
// Check if client is already in the channel
|
||||
if _, exists := ch.clients[nick]; exists {
|
||||
// Client already in channel, don't add again
|
||||
return
|
||||
}
|
||||
|
||||
ch.clients[nick] = client
|
||||
client.AddChannel(ch)
|
||||
|
||||
// First user gets configured founder mode (default: operator)
|
||||
if len(ch.clients) == 1 {
|
||||
// Get the configured founder mode from server config
|
||||
founderMode := "o" // Default fallback
|
||||
if client.server != nil && client.server.config != nil {
|
||||
founderMode = client.server.config.Channels.FounderMode
|
||||
}
|
||||
|
||||
// Apply the appropriate mode based on configuration
|
||||
switch founderMode {
|
||||
case "q":
|
||||
ch.owners[nick] = client
|
||||
case "a":
|
||||
ch.admins[nick] = client
|
||||
case "o":
|
||||
ch.operators[nick] = client
|
||||
case "h":
|
||||
ch.halfops[nick] = client
|
||||
case "v":
|
||||
ch.voices[nick] = client
|
||||
default:
|
||||
// Invalid config, fallback to operator
|
||||
ch.operators[nick] = client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) RemoveClient(client *Client) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
delete(ch.clients, nick)
|
||||
delete(ch.operators, nick)
|
||||
delete(ch.halfops, nick)
|
||||
delete(ch.voices, nick)
|
||||
delete(ch.owners, nick)
|
||||
delete(ch.admins, nick)
|
||||
client.RemoveChannel(ch.name)
|
||||
}
|
||||
|
||||
func (ch *Channel) HasClient(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
_, exists := ch.clients[strings.ToLower(client.Nick())]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (ch *Channel) IsOperator(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
_, exists := ch.operators[strings.ToLower(client.Nick())]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (ch *Channel) IsVoice(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
_, exists := ch.voices[strings.ToLower(client.Nick())]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (ch *Channel) IsHalfop(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
_, exists := ch.halfops[strings.ToLower(client.Nick())]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (ch *Channel) IsOwner(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
_, exists := ch.owners[strings.ToLower(client.Nick())]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (ch *Channel) IsQuieted(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.isQuietedUnsafe(client)
|
||||
}
|
||||
|
||||
func (ch *Channel) isQuietedUnsafe(client *Client) bool {
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
|
||||
for _, quiet := range ch.quietList {
|
||||
if ch.matchesBanMask(client, quiet, hostmask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ch *Channel) SetOperator(client *Client, isOp bool) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
if isOp {
|
||||
ch.operators[nick] = client
|
||||
} else {
|
||||
delete(ch.operators, nick)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) SetVoice(client *Client, hasVoice bool) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
if hasVoice {
|
||||
ch.voices[nick] = client
|
||||
} else {
|
||||
delete(ch.voices, nick)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) SetHalfop(client *Client, isHalfop bool) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
if isHalfop {
|
||||
ch.halfops[nick] = client
|
||||
} else {
|
||||
delete(ch.halfops, nick)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) SetOwner(client *Client, isOwner bool) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
if isOwner {
|
||||
ch.owners[nick] = client
|
||||
} else {
|
||||
delete(ch.owners, nick)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) IsAdmin(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
_, exists := ch.admins[strings.ToLower(client.Nick())]
|
||||
return exists
|
||||
}
|
||||
|
||||
func (ch *Channel) SetAdmin(client *Client, isAdmin bool) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
nick := strings.ToLower(client.Nick())
|
||||
if isAdmin {
|
||||
ch.admins[nick] = client
|
||||
} else {
|
||||
delete(ch.admins, nick)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) GetClients() []*Client {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
clients := make([]*Client, 0, len(ch.clients))
|
||||
for _, client := range ch.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
func (ch *Channel) GetClientCount() int {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return len(ch.clients)
|
||||
}
|
||||
|
||||
func (ch *Channel) UserCount() int {
|
||||
return ch.GetClientCount()
|
||||
}
|
||||
|
||||
func (ch *Channel) Broadcast(message string, exclude *Client) {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
for _, client := range ch.clients {
|
||||
if exclude != nil && client.Nick() == exclude.Nick() {
|
||||
continue
|
||||
}
|
||||
client.SendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) BroadcastFrom(source, message string, exclude *Client) {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
for _, client := range ch.clients {
|
||||
if exclude != nil && client.Nick() == exclude.Nick() {
|
||||
continue
|
||||
}
|
||||
client.SendFrom(source, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) HasMode(mode rune) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.modes[mode]
|
||||
}
|
||||
|
||||
func (ch *Channel) SetMode(mode rune, set bool) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
if set {
|
||||
ch.modes[mode] = true
|
||||
} else {
|
||||
delete(ch.modes, mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) GetModes() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
var modes []rune
|
||||
for mode := range ch.modes {
|
||||
modes = append(modes, mode)
|
||||
}
|
||||
|
||||
if len(modes) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "+" + string(modes)
|
||||
}
|
||||
|
||||
func (ch *Channel) CanSendMessage(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
// Check if user is quieted first
|
||||
if ch.isQuietedUnsafe(client) {
|
||||
// Only owners, operators, and halfops can speak when quieted
|
||||
nick := strings.ToLower(client.Nick())
|
||||
_, isOwner := ch.owners[nick]
|
||||
_, isOp := ch.operators[nick]
|
||||
_, isHalfop := ch.halfops[nick]
|
||||
|
||||
if !isOwner && !isOp && !isHalfop {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If channel is not moderated, anyone in the channel can send
|
||||
if !ch.modes['m'] {
|
||||
return true
|
||||
}
|
||||
|
||||
// In moderated channels, only owners, operators, halfops and voiced users can send messages
|
||||
nick := strings.ToLower(client.Nick())
|
||||
_, isOwner := ch.owners[nick]
|
||||
_, isOp := ch.operators[nick]
|
||||
_, isHalfop := ch.halfops[nick]
|
||||
_, hasVoice := ch.voices[nick]
|
||||
|
||||
return isOwner || isOp || isHalfop || hasVoice
|
||||
}
|
||||
|
||||
func (ch *Channel) Key() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.key
|
||||
}
|
||||
|
||||
func (ch *Channel) SetKey(key string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.key = key
|
||||
}
|
||||
|
||||
func (ch *Channel) Limit() int {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.limit
|
||||
}
|
||||
|
||||
func (ch *Channel) SetLimit(limit int) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.limit = limit
|
||||
}
|
||||
|
||||
func (ch *Channel) GetNamesReply() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
var names []string
|
||||
for _, client := range ch.clients {
|
||||
prefix := ""
|
||||
if ch.IsOwner(client) {
|
||||
prefix = "~"
|
||||
} else if ch.IsOperator(client) {
|
||||
prefix = "@"
|
||||
} else if ch.IsHalfop(client) {
|
||||
prefix = "%"
|
||||
} else if ch.IsVoice(client) {
|
||||
prefix = "+"
|
||||
}
|
||||
names = append(names, prefix+client.Nick())
|
||||
}
|
||||
|
||||
return strings.Join(names, " ")
|
||||
}
|
||||
|
||||
func (ch *Channel) CanSpeak(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
// If channel is not moderated, anyone can speak
|
||||
if !ch.modes['m'] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Operators and voiced users can always speak
|
||||
return ch.IsOperator(client) || ch.IsVoice(client)
|
||||
}
|
||||
|
||||
func (ch *Channel) CanJoin(client *Client, key string) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
// Check if invite-only
|
||||
if ch.modes['i'] {
|
||||
// Check invite list
|
||||
for _, mask := range ch.inviteList {
|
||||
if ch.matchesMask(client.Prefix(), mask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check key
|
||||
if ch.modes['k'] && ch.key != key {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check limit
|
||||
if ch.modes['l'] && len(ch.clients) >= ch.limit {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check ban list
|
||||
for _, mask := range ch.banList {
|
||||
if ch.matchesMask(client.Prefix(), mask) {
|
||||
// Check exception list
|
||||
for _, exceptMask := range ch.exceptList {
|
||||
if ch.matchesMask(client.Prefix(), exceptMask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ch *Channel) matchesMask(target, mask string) bool {
|
||||
// Simple mask matching - should be enhanced for production
|
||||
return strings.Contains(strings.ToLower(target), strings.ToLower(mask))
|
||||
}
|
||||
|
||||
func (ch *Channel) AddBan(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.banList = append(ch.banList, mask)
|
||||
}
|
||||
|
||||
func (ch *Channel) RemoveBan(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
for i, ban := range ch.banList {
|
||||
if ban == mask {
|
||||
ch.banList = append(ch.banList[:i], ch.banList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) GetBans() []string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
bans := make([]string, len(ch.banList))
|
||||
copy(bans, ch.banList)
|
||||
return bans
|
||||
}
|
||||
|
||||
// Extended ban list management
|
||||
func (ch *Channel) AddQuiet(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.quietList = append(ch.quietList, mask)
|
||||
}
|
||||
|
||||
func (ch *Channel) RemoveQuiet(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
for i, quiet := range ch.quietList {
|
||||
if quiet == mask {
|
||||
ch.quietList = append(ch.quietList[:i], ch.quietList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) GetQuiets() []string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
quiets := make([]string, len(ch.quietList))
|
||||
copy(quiets, ch.quietList)
|
||||
return quiets
|
||||
}
|
||||
|
||||
func (ch *Channel) AddExcept(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.exceptList = append(ch.exceptList, mask)
|
||||
}
|
||||
|
||||
func (ch *Channel) RemoveExcept(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
for i, except := range ch.exceptList {
|
||||
if except == mask {
|
||||
ch.exceptList = append(ch.exceptList[:i], ch.exceptList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) GetExcepts() []string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
excepts := make([]string, len(ch.exceptList))
|
||||
copy(excepts, ch.exceptList)
|
||||
return excepts
|
||||
}
|
||||
|
||||
func (ch *Channel) AddInviteException(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.inviteList = append(ch.inviteList, mask)
|
||||
}
|
||||
|
||||
func (ch *Channel) RemoveInviteException(mask string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
|
||||
for i, invite := range ch.inviteList {
|
||||
if invite == mask {
|
||||
ch.inviteList = append(ch.inviteList[:i], ch.inviteList[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ch *Channel) GetInviteExceptions() []string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
invites := make([]string, len(ch.inviteList))
|
||||
copy(invites, ch.inviteList)
|
||||
return invites
|
||||
}
|
||||
|
||||
// IsExempt checks if a client is exempt from bans
|
||||
func (ch *Channel) IsExempt(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
|
||||
for _, except := range ch.exceptList {
|
||||
if ch.matchesBanMask(client, except, hostmask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CanJoinInviteOnly checks if a client can join an invite-only channel
|
||||
func (ch *Channel) CanJoinInviteOnly(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
|
||||
for _, invite := range ch.inviteList {
|
||||
if ch.matchesBanMask(client, invite, hostmask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ch *Channel) Created() time.Time {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.created
|
||||
}
|
||||
|
||||
// IsBanned checks if a client matches any ban mask in the channel
|
||||
func (ch *Channel) IsBanned(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
// Check exemptions first - if exempt, not banned
|
||||
if ch.isExemptUnsafe(client) {
|
||||
return false
|
||||
}
|
||||
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
|
||||
for _, ban := range ch.banList {
|
||||
if ch.matchesBanMask(client, ban, hostmask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isExemptUnsafe checks exemptions without locking (internal use)
|
||||
func (ch *Channel) isExemptUnsafe(client *Client) bool {
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
|
||||
for _, except := range ch.exceptList {
|
||||
if ch.matchesBanMask(client, except, hostmask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesBanMask checks if a client matches a ban mask (supports extended bans)
|
||||
func (ch *Channel) matchesBanMask(client *Client, banMask, hostmask string) bool {
|
||||
// Check for extended ban format: ~type:parameter or ~type parameter
|
||||
if strings.HasPrefix(banMask, "~") {
|
||||
return ch.matchesExtendedBan(client, banMask)
|
||||
}
|
||||
|
||||
// Traditional hostmask ban
|
||||
return matchWildcard(banMask, hostmask)
|
||||
}
|
||||
|
||||
// matchesExtendedBan handles extended ban types
|
||||
func (ch *Channel) matchesExtendedBan(client *Client, extban string) bool {
|
||||
if len(extban) < 2 || extban[0] != '~' {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse ~type:parameter or ~type parameter format
|
||||
var banType string
|
||||
var parameter string
|
||||
|
||||
if strings.Contains(extban, ":") {
|
||||
// Format: ~type:parameter
|
||||
parts := strings.SplitN(extban[1:], ":", 2)
|
||||
banType = parts[0]
|
||||
if len(parts) > 1 {
|
||||
parameter = parts[1]
|
||||
}
|
||||
} else {
|
||||
// Format: ~type parameter (space separated)
|
||||
parts := strings.Fields(extban[1:])
|
||||
if len(parts) > 0 {
|
||||
banType = parts[0]
|
||||
if len(parts) > 1 {
|
||||
parameter = strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch banType {
|
||||
case "a": // Account ban: ~a:accountname or ~a accountname
|
||||
if parameter == "" {
|
||||
// ~a with no parameter bans unregistered users
|
||||
return client.account == ""
|
||||
}
|
||||
// ~a:account bans specific account
|
||||
return client.account == parameter
|
||||
|
||||
case "c": // Channel ban: ~c:#channel - ban users in another channel
|
||||
if parameter == "" || !strings.HasPrefix(parameter, "#") {
|
||||
return false
|
||||
}
|
||||
targetChannel := client.server.GetChannel(parameter)
|
||||
return targetChannel != nil && targetChannel.HasClient(client)
|
||||
|
||||
case "j": // Join prevent: ~j:#channel - prevent joining if in another channel
|
||||
if parameter == "" || !strings.HasPrefix(parameter, "#") {
|
||||
return false
|
||||
}
|
||||
targetChannel := client.server.GetChannel(parameter)
|
||||
return targetChannel != nil && targetChannel.HasClient(client)
|
||||
|
||||
case "n": // Nick pattern ban: ~n:pattern
|
||||
if parameter == "" {
|
||||
return false
|
||||
}
|
||||
return matchWildcard(parameter, client.Nick())
|
||||
|
||||
case "q": // Quiet ban: ~q:mask - this is a special case for quiet functionality
|
||||
// When used in regular ban list, ~q acts as a quiet
|
||||
if parameter == "" {
|
||||
// ~q with no parameter quiets everyone
|
||||
return true
|
||||
}
|
||||
// ~q:mask quiets matching users - check against hostmask pattern
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
return matchWildcard(parameter, hostmask)
|
||||
|
||||
case "r": // Real name ban: ~r:pattern
|
||||
if parameter == "" {
|
||||
return false
|
||||
}
|
||||
return matchWildcard(parameter, client.realname)
|
||||
|
||||
case "s": // Server ban: ~s:servername
|
||||
if parameter == "" {
|
||||
return false
|
||||
}
|
||||
return matchWildcard(parameter, client.server.config.Server.Name)
|
||||
|
||||
case "o": // Operator ban: ~o (bans all opers)
|
||||
return client.IsOper()
|
||||
|
||||
case "z": // Non-SSL ban: ~z (bans non-SSL users)
|
||||
return !client.ssl
|
||||
|
||||
case "Z": // SSL-only ban: ~Z (bans SSL users)
|
||||
return client.ssl
|
||||
|
||||
case "u": // Username pattern ban: ~u:pattern
|
||||
if parameter == "" {
|
||||
return false
|
||||
}
|
||||
return matchWildcard(parameter, client.User())
|
||||
|
||||
case "h": // Hostname pattern ban: ~h:pattern
|
||||
if parameter == "" {
|
||||
return false
|
||||
}
|
||||
return matchWildcard(parameter, client.Host())
|
||||
|
||||
case "i": // IP ban: ~i:ip/cidr
|
||||
if parameter == "" {
|
||||
return false
|
||||
}
|
||||
// Simple IP matching for now (could be enhanced with CIDR)
|
||||
return matchWildcard(parameter, client.Host())
|
||||
|
||||
case "R": // Registered only: ~R (bans unregistered users)
|
||||
return client.account == ""
|
||||
|
||||
case "m": // Mute ban: ~m:mask - similar to quiet
|
||||
if parameter == "" {
|
||||
return true
|
||||
}
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
return matchWildcard(parameter, hostmask)
|
||||
|
||||
default:
|
||||
// Unknown extended ban type
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsInvited checks if a client is on the invite list for the channel
|
||||
func (ch *Channel) IsInvited(client *Client) bool {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
|
||||
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
|
||||
|
||||
for _, invite := range ch.inviteList {
|
||||
if matchWildcard(invite, hostmask) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchWildcard checks if a pattern with wildcards (* and ?) matches a string
|
||||
func matchWildcard(pattern, str string) bool {
|
||||
matched, _ := filepath.Match(strings.ToLower(pattern), strings.ToLower(str))
|
||||
return matched
|
||||
}
|
||||
|
||||
// SetFloodSettings sets the flood protection settings
|
||||
func (ch *Channel) SetFloodSettings(settings string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.floodSettings = settings
|
||||
}
|
||||
|
||||
// GetFloodSettings returns the flood protection settings
|
||||
func (ch *Channel) GetFloodSettings() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.floodSettings
|
||||
}
|
||||
|
||||
// SetJoinThrottle sets the join throttling settings
|
||||
func (ch *Channel) SetJoinThrottle(settings string) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
ch.joinThrottle = settings
|
||||
}
|
||||
|
||||
// GetJoinThrottle returns the join throttling settings
|
||||
func (ch *Channel) GetJoinThrottle() string {
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
return ch.joinThrottle
|
||||
}
|
||||
106
cmd/techircd/main.go
Normal file
106
cmd/techircd/main.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Config represents the server configuration.
|
||||
// You should replace the fields below with your actual configuration structure.
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Version string
|
||||
Listen struct {
|
||||
Host string
|
||||
Port int
|
||||
EnableSSL bool
|
||||
}
|
||||
SSL struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy implementations for missing functions.
|
||||
// Replace these with your actual implementations.
|
||||
func ParseConfig(file *os.File) (*Config, error) {
|
||||
return &Config{}, nil
|
||||
}
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
func SaveConfig(cfg *Config, path string) error {
|
||||
return nil
|
||||
}
|
||||
func (c *Config) Validate() error {
|
||||
return nil
|
||||
}
|
||||
func (c *Config) SanitizeConfig() {}
|
||||
func NewServer(cfg *Config) *Server {
|
||||
return &Server{}
|
||||
}
|
||||
|
||||
type Server struct{}
|
||||
|
||||
func (s *Server) Shutdown() {}
|
||||
func (s *Server) Start() error { return nil }
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return ParseConfig(file)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
configFile := flag.String("config", "config.json", "Path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(*configFile)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load config from %s, using defaults: %v", *configFile, err)
|
||||
// Create default config if file doesn't exist
|
||||
config = DefaultConfig()
|
||||
if err := SaveConfig(config, *configFile); err != nil {
|
||||
log.Printf("Failed to save default config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and sanitize configuration
|
||||
if err := config.Validate(); err != nil {
|
||||
log.Fatalf("Configuration validation failed: %v", err)
|
||||
}
|
||||
config.SanitizeConfig()
|
||||
log.Println("Configuration validated successfully")
|
||||
|
||||
// Create and start the server
|
||||
server := NewServer(config)
|
||||
|
||||
// Handle graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-c
|
||||
log.Println("Shutting down server...")
|
||||
server.Shutdown()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Start the server
|
||||
log.Printf("Starting TechIRCd %s on %s:%d", config.Server.Version, config.Server.Listen.Host, config.Server.Listen.Port)
|
||||
if config.Server.Listen.EnableSSL {
|
||||
log.Printf("SSL enabled with cert: %s, key: %s", config.Server.SSL.CertFile, config.Server.SSL.KeyFile)
|
||||
}
|
||||
if err := server.Start(); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
4098
commands.go
Normal file
4098
commands.go
Normal file
File diff suppressed because it is too large
Load Diff
407
config.go
Normal file
407
config.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Name string `json:"name"`
|
||||
Network string `json:"network"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
AdminInfo string `json:"admin_info"`
|
||||
Listen struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
SSLPort int `json:"ssl_port"`
|
||||
EnableSSL bool `json:"enable_ssl"`
|
||||
} `json:"listen"`
|
||||
SSL struct {
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
RequireSSL bool `json:"require_ssl"`
|
||||
} `json:"ssl"`
|
||||
} `json:"server"`
|
||||
|
||||
Limits struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
MaxChannels int `json:"max_channels"`
|
||||
MaxChannelUsers int `json:"max_channel_users"`
|
||||
MaxNickLength int `json:"max_nick_length"`
|
||||
MaxChannelLength int `json:"max_channel_length"`
|
||||
MaxTopicLength int `json:"max_topic_length"`
|
||||
MaxKickLength int `json:"max_kick_length"`
|
||||
MaxAwayLength int `json:"max_away_length"`
|
||||
PingTimeout int `json:"ping_timeout"`
|
||||
RegistrationTimeout int `json:"registration_timeout"`
|
||||
FloodLines int `json:"flood_lines"`
|
||||
FloodSeconds int `json:"flood_seconds"`
|
||||
} `json:"limits"`
|
||||
|
||||
Features struct {
|
||||
EnableOper bool `json:"enable_oper"`
|
||||
EnableServices bool `json:"enable_services"`
|
||||
EnableModes bool `json:"enable_modes"`
|
||||
EnableCTCP bool `json:"enable_ctcp"`
|
||||
EnableDCC bool `json:"enable_dcc"`
|
||||
CaseMapping string `json:"case_mapping"`
|
||||
} `json:"features"`
|
||||
|
||||
NickChangeNotification struct {
|
||||
ShowToEveryone bool `json:"to_everyone"`
|
||||
ShowToOpers bool `json:"to_opers"`
|
||||
ShowToSelf bool `json:"to_self"`
|
||||
} `json:"nick_change_notification"`
|
||||
|
||||
Privacy struct {
|
||||
HideHostsFromUsers bool `json:"hide_hosts_from_users"`
|
||||
OperBypassHostHide bool `json:"oper_bypass_host_hide"`
|
||||
MaskedHostSuffix string `json:"masked_host_suffix"`
|
||||
} `json:"privacy"`
|
||||
|
||||
WhoisFeatures struct {
|
||||
// Basic information visibility
|
||||
ShowUserModes struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_user_modes"`
|
||||
|
||||
ShowSSLStatus struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_ssl_status"`
|
||||
|
||||
ShowIdleTime struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_idle_time"`
|
||||
|
||||
ShowSignonTime struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_signon_time"`
|
||||
|
||||
ShowRealHost struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_real_host"`
|
||||
|
||||
ShowChannels struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
HideSecret bool `json:"hide_secret_channels"`
|
||||
HidePrivate bool `json:"hide_private_channels"`
|
||||
ShowMembership bool `json:"show_membership_levels"`
|
||||
} `json:"show_channels"`
|
||||
|
||||
ShowOperClass struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_oper_class"`
|
||||
|
||||
ShowClientInfo struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_client_info"`
|
||||
|
||||
ShowAccountName struct {
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
} `json:"show_account_name"`
|
||||
|
||||
// Advanced/unique features
|
||||
ShowActivityStats bool `json:"show_activity_stats"`
|
||||
ShowGitHubIntegration bool `json:"show_github_integration"`
|
||||
ShowGeolocation bool `json:"show_geolocation"`
|
||||
ShowPerformanceStats bool `json:"show_performance_stats"`
|
||||
ShowDeviceInfo bool `json:"show_device_info"`
|
||||
ShowSocialGraph bool `json:"show_social_graph"`
|
||||
ShowSecurityScore bool `json:"show_security_score"`
|
||||
|
||||
// Custom fields
|
||||
CustomFields []struct {
|
||||
Name string `json:"name"`
|
||||
ToEveryone bool `json:"to_everyone"`
|
||||
ToOpers bool `json:"to_opers"`
|
||||
ToSelf bool `json:"to_self"`
|
||||
Format string `json:"format"`
|
||||
Description string `json:"description"`
|
||||
} `json:"custom_fields"`
|
||||
} `json:"whois_features"`
|
||||
|
||||
Channels struct {
|
||||
DefaultModes string `json:"default_modes"`
|
||||
AutoJoin []string `json:"auto_join"`
|
||||
AdminChannels []string `json:"admin_channels"`
|
||||
FounderMode string `json:"founder_mode"` // Mode given to first user joining a channel: "o", "a", "q"
|
||||
AllowedModes struct {
|
||||
Voice bool `json:"voice"` // +v
|
||||
Halfop bool `json:"halfop"` // +h
|
||||
Operator bool `json:"operator"` // +o
|
||||
Admin bool `json:"admin"` // +a
|
||||
Owner bool `json:"owner"` // +q
|
||||
} `json:"allowed_modes"`
|
||||
Modes struct {
|
||||
BanListSize int `json:"ban_list_size"`
|
||||
ExceptListSize int `json:"except_list_size"`
|
||||
InviteListSize int `json:"invite_list_size"`
|
||||
} `json:"modes"`
|
||||
} `json:"channels"`
|
||||
|
||||
Opers []struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Class string `json:"class"`
|
||||
Flags []string `json:"flags"`
|
||||
} `json:"opers"`
|
||||
|
||||
OperConfig struct {
|
||||
ConfigFile string `json:"config_file"`
|
||||
Enable bool `json:"enable"`
|
||||
} `json:"oper_config"`
|
||||
|
||||
MOTD []string `json:"motd"`
|
||||
|
||||
Linking struct {
|
||||
Enable bool `json:"enable"`
|
||||
ServerPort int `json:"server_port"`
|
||||
Password string `json:"password"`
|
||||
Hub bool `json:"hub"`
|
||||
AutoConnect bool `json:"auto_connect"`
|
||||
Links []struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Password string `json:"password"`
|
||||
AutoConnect bool `json:"auto_connect"`
|
||||
Hub bool `json:"hub"`
|
||||
Description string `json:"description"`
|
||||
} `json:"links"`
|
||||
} `json:"linking"`
|
||||
|
||||
Logging struct {
|
||||
Level string `json:"level"`
|
||||
File string `json:"file"`
|
||||
MaxSize int `json:"max_size"`
|
||||
MaxBackups int `json:"max_backups"`
|
||||
MaxAge int `json:"max_age"`
|
||||
} `json:"logging"`
|
||||
}
|
||||
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %v", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (c *Config) PingTimeoutDuration() time.Duration {
|
||||
return time.Duration(c.Limits.PingTimeout) * time.Second
|
||||
}
|
||||
|
||||
func (c *Config) RegistrationTimeoutDuration() time.Duration {
|
||||
return time.Duration(c.Limits.RegistrationTimeout) * time.Second
|
||||
}
|
||||
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: struct {
|
||||
Name string `json:"name"`
|
||||
Network string `json:"network"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
AdminInfo string `json:"admin_info"`
|
||||
Listen struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
SSLPort int `json:"ssl_port"`
|
||||
EnableSSL bool `json:"enable_ssl"`
|
||||
} `json:"listen"`
|
||||
SSL struct {
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
RequireSSL bool `json:"require_ssl"`
|
||||
} `json:"ssl"`
|
||||
}{
|
||||
Name: "TechIRCd",
|
||||
Network: "TechNet",
|
||||
Description: "A modern IRC server written in Go",
|
||||
Version: "1.0.0",
|
||||
AdminInfo: "admin@example.com",
|
||||
Listen: struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
SSLPort int `json:"ssl_port"`
|
||||
EnableSSL bool `json:"enable_ssl"`
|
||||
}{
|
||||
Host: "localhost",
|
||||
Port: 6667,
|
||||
SSLPort: 6697,
|
||||
EnableSSL: false,
|
||||
},
|
||||
SSL: struct {
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
RequireSSL bool `json:"require_ssl"`
|
||||
}{
|
||||
CertFile: "server.crt",
|
||||
KeyFile: "server.key",
|
||||
RequireSSL: false,
|
||||
},
|
||||
},
|
||||
Limits: struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
MaxChannels int `json:"max_channels"`
|
||||
MaxChannelUsers int `json:"max_channel_users"`
|
||||
MaxNickLength int `json:"max_nick_length"`
|
||||
MaxChannelLength int `json:"max_channel_length"`
|
||||
MaxTopicLength int `json:"max_topic_length"`
|
||||
MaxKickLength int `json:"max_kick_length"`
|
||||
MaxAwayLength int `json:"max_away_length"`
|
||||
PingTimeout int `json:"ping_timeout"`
|
||||
RegistrationTimeout int `json:"registration_timeout"`
|
||||
FloodLines int `json:"flood_lines"`
|
||||
FloodSeconds int `json:"flood_seconds"`
|
||||
}{
|
||||
MaxClients: 1000,
|
||||
MaxChannels: 100,
|
||||
MaxChannelUsers: 500,
|
||||
MaxNickLength: 30,
|
||||
MaxChannelLength: 50,
|
||||
MaxTopicLength: 307,
|
||||
MaxKickLength: 307,
|
||||
MaxAwayLength: 307,
|
||||
PingTimeout: 300,
|
||||
RegistrationTimeout: 60,
|
||||
FloodLines: 20,
|
||||
FloodSeconds: 10,
|
||||
},
|
||||
Features: struct {
|
||||
EnableOper bool `json:"enable_oper"`
|
||||
EnableServices bool `json:"enable_services"`
|
||||
EnableModes bool `json:"enable_modes"`
|
||||
EnableCTCP bool `json:"enable_ctcp"`
|
||||
EnableDCC bool `json:"enable_dcc"`
|
||||
CaseMapping string `json:"case_mapping"`
|
||||
}{
|
||||
EnableOper: true,
|
||||
EnableServices: false,
|
||||
EnableModes: true,
|
||||
EnableCTCP: true,
|
||||
EnableDCC: false,
|
||||
CaseMapping: "rfc1459",
|
||||
},
|
||||
Channels: struct {
|
||||
DefaultModes string `json:"default_modes"`
|
||||
AutoJoin []string `json:"auto_join"`
|
||||
AdminChannels []string `json:"admin_channels"`
|
||||
FounderMode string `json:"founder_mode"` // Mode given to first user joining a channel: "o", "a", "q"
|
||||
AllowedModes struct {
|
||||
Voice bool `json:"voice"` // +v
|
||||
Halfop bool `json:"halfop"` // +h
|
||||
Operator bool `json:"operator"` // +o
|
||||
Admin bool `json:"admin"` // +a
|
||||
Owner bool `json:"owner"` // +q
|
||||
} `json:"allowed_modes"`
|
||||
Modes struct {
|
||||
BanListSize int `json:"ban_list_size"`
|
||||
ExceptListSize int `json:"except_list_size"`
|
||||
InviteListSize int `json:"invite_list_size"`
|
||||
} `json:"modes"`
|
||||
}{
|
||||
DefaultModes: "+nt",
|
||||
AutoJoin: []string{"#general"},
|
||||
AdminChannels: []string{"#admin"},
|
||||
FounderMode: "o", // Default to operator mode for channel founders
|
||||
AllowedModes: struct {
|
||||
Voice bool `json:"voice"`
|
||||
Halfop bool `json:"halfop"`
|
||||
Operator bool `json:"operator"`
|
||||
Admin bool `json:"admin"`
|
||||
Owner bool `json:"owner"`
|
||||
}{
|
||||
Voice: true,
|
||||
Halfop: true,
|
||||
Operator: true,
|
||||
Admin: true,
|
||||
Owner: true,
|
||||
},
|
||||
Modes: struct {
|
||||
BanListSize int `json:"ban_list_size"`
|
||||
ExceptListSize int `json:"except_list_size"`
|
||||
InviteListSize int `json:"invite_list_size"`
|
||||
}{
|
||||
BanListSize: 100,
|
||||
ExceptListSize: 100,
|
||||
InviteListSize: 100,
|
||||
},
|
||||
},
|
||||
Opers: []struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Class string `json:"class"`
|
||||
Flags []string `json:"flags"`
|
||||
}{
|
||||
{
|
||||
Name: "admin",
|
||||
Password: "changeme",
|
||||
Host: "*@localhost",
|
||||
Class: "admin",
|
||||
Flags: []string{"global_kill", "remote", "connect", "squit"},
|
||||
},
|
||||
},
|
||||
MOTD: []string{
|
||||
"Welcome to TechIRCd!",
|
||||
"A modern IRC server written in Go",
|
||||
"Enjoy your stay on TechNet!",
|
||||
},
|
||||
Logging: struct {
|
||||
Level string `json:"level"`
|
||||
File string `json:"file"`
|
||||
MaxSize int `json:"max_size"`
|
||||
MaxBackups int `json:"max_backups"`
|
||||
MaxAge int `json:"max_age"`
|
||||
}{
|
||||
Level: "info",
|
||||
File: "techircd.log",
|
||||
MaxSize: 100,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func SaveConfig(config *Config, filename string) error {
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
163
config.json
Normal file
163
config.json
Normal file
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"server": {
|
||||
"name": "TechIRCd",
|
||||
"network": "TechNet",
|
||||
"description": "A modern IRC server written in Go",
|
||||
"version": "1.0.0",
|
||||
"admin_info": "admin@example.com",
|
||||
"listen": {
|
||||
"host": "localhost",
|
||||
"port": 6667,
|
||||
"ssl_port": 6697,
|
||||
"enable_ssl": false
|
||||
},
|
||||
"ssl": {
|
||||
"cert_file": "server.crt",
|
||||
"key_file": "server.key",
|
||||
"require_ssl": false
|
||||
}
|
||||
},
|
||||
"limits": {
|
||||
"max_clients": 1000,
|
||||
"max_channels": 100,
|
||||
"max_channel_users": 500,
|
||||
"max_nick_length": 30,
|
||||
"max_channel_length": 50,
|
||||
"max_topic_length": 307,
|
||||
"max_kick_length": 307,
|
||||
"max_away_length": 307,
|
||||
"ping_timeout": 300,
|
||||
"registration_timeout": 60,
|
||||
"flood_lines": 20,
|
||||
"flood_seconds": 10
|
||||
},
|
||||
"features": {
|
||||
"enable_oper": true,
|
||||
"enable_services": false,
|
||||
"enable_modes": true,
|
||||
"enable_ctcp": true,
|
||||
"enable_dcc": false,
|
||||
"case_mapping": "rfc1459"
|
||||
},
|
||||
"nick_change_notification": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"channels": {
|
||||
"default_modes": "+nt",
|
||||
"auto_join": [
|
||||
"#general"
|
||||
],
|
||||
"admin_channels": [
|
||||
"#admin"
|
||||
],
|
||||
"founder_mode": "o",
|
||||
"allowed_modes": {
|
||||
"voice": true,
|
||||
"halfop": true,
|
||||
"operator": true,
|
||||
"admin": true,
|
||||
"owner": true
|
||||
},
|
||||
"modes": {
|
||||
"ban_list_size": 100,
|
||||
"except_list_size": 100,
|
||||
"invite_list_size": 100
|
||||
}
|
||||
},
|
||||
"opers": [
|
||||
{
|
||||
"name": "admin",
|
||||
"password": "changeme",
|
||||
"host": "*@localhost",
|
||||
"class": "admin",
|
||||
"flags": [
|
||||
"global_kill",
|
||||
"remote",
|
||||
"connect",
|
||||
"squit"
|
||||
]
|
||||
}
|
||||
],
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"server_port": 6697,
|
||||
"password": "linkpassword123",
|
||||
"hub": false,
|
||||
"auto_connect": false,
|
||||
"links": [
|
||||
{
|
||||
"name": "hub.technet.org",
|
||||
"host": "127.0.0.1",
|
||||
"port": 6697,
|
||||
"password": "linkpassword123",
|
||||
"auto_connect": false,
|
||||
"hub": true,
|
||||
"description": "TechNet Hub Server"
|
||||
}
|
||||
]
|
||||
},
|
||||
"whois_features": {
|
||||
"show_user_modes": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_ssl_status": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_idle_time": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_signon_time": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_real_host": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_channels": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true,
|
||||
"hide_secret_channels": true,
|
||||
"hide_private_channels": false,
|
||||
"show_membership_levels": true
|
||||
},
|
||||
"show_oper_class": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_client_info": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": false
|
||||
},
|
||||
"show_account_name": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
},
|
||||
"motd": [
|
||||
"Welcome to TechIRCd!",
|
||||
"A modern IRC server written in Go",
|
||||
"Enjoy your stay on TechNet!"
|
||||
],
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "techircd.log",
|
||||
"max_size": 100,
|
||||
"max_backups": 3,
|
||||
"max_age": 28
|
||||
}
|
||||
}
|
||||
150
configs/config.json
Normal file
150
configs/config.json
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
"server": {
|
||||
"name": "TechIRCd",
|
||||
"network": "TechNet",
|
||||
"description": "A modern IRC server written in Go",
|
||||
"version": "1.0.0",
|
||||
"admin_info": "admin@example.com",
|
||||
"listen": {
|
||||
"host": "localhost",
|
||||
"port": 6667,
|
||||
"ssl_port": 6697,
|
||||
"enable_ssl": false
|
||||
},
|
||||
"ssl": {
|
||||
"cert_file": "server.crt",
|
||||
"key_file": "server.key",
|
||||
"require_ssl": false
|
||||
}
|
||||
},
|
||||
"limits": {
|
||||
"max_clients": 1000,
|
||||
"max_channels": 100,
|
||||
"max_channel_users": 500,
|
||||
"max_nick_length": 30,
|
||||
"max_channel_length": 50,
|
||||
"max_topic_length": 307,
|
||||
"max_kick_length": 307,
|
||||
"max_away_length": 307,
|
||||
"ping_timeout": 300,
|
||||
"registration_timeout": 60,
|
||||
"flood_lines": 10,
|
||||
"flood_seconds": 60
|
||||
},
|
||||
"features": {
|
||||
"enable_oper": true,
|
||||
"enable_services": false,
|
||||
"enable_modes": true,
|
||||
"enable_ctcp": true,
|
||||
"enable_dcc": false,
|
||||
"case_mapping": "rfc1459"
|
||||
},
|
||||
"privacy": {
|
||||
"hide_hosts_from_users": true,
|
||||
"oper_bypass_host_hide": true,
|
||||
"masked_host_suffix": "users.technet"
|
||||
},
|
||||
"whois_features": {
|
||||
"show_user_modes": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_ssl_status": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_idle_time": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_signon_time": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_real_host": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_channels": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true,
|
||||
"hide_secret_channels": true,
|
||||
"hide_private_channels": false,
|
||||
"show_membership_levels": true
|
||||
},
|
||||
"show_oper_class": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_client_info": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": false
|
||||
},
|
||||
"show_account_name": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
},
|
||||
"show_activity_stats": false,
|
||||
"show_github_integration": false,
|
||||
"show_geolocation": false,
|
||||
"show_performance_stats": false,
|
||||
"show_device_info": false,
|
||||
"show_social_graph": false,
|
||||
"show_security_score": false,
|
||||
"custom_fields": []
|
||||
},
|
||||
"channels": {
|
||||
"default_modes": "+nt",
|
||||
"auto_join": [
|
||||
"#general"
|
||||
],
|
||||
"admin_channels": [
|
||||
"#admin"
|
||||
],
|
||||
"founder_mode": "o",
|
||||
"modes": {
|
||||
"ban_list_size": 100,
|
||||
"except_list_size": 100,
|
||||
"invite_list_size": 100
|
||||
}
|
||||
},
|
||||
"opers": [
|
||||
{
|
||||
"name": "admin",
|
||||
"password": "your_secure_password_here",
|
||||
"host": "*@localhost",
|
||||
"class": "admin",
|
||||
"flags": [
|
||||
"global_kill",
|
||||
"remote",
|
||||
"connect",
|
||||
"squit"
|
||||
]
|
||||
}
|
||||
],
|
||||
"oper_config": {
|
||||
"config_file": "configs/opers.conf",
|
||||
"enable": true
|
||||
},
|
||||
"motd": [
|
||||
"Welcome to TechIRCd!",
|
||||
"A modern IRC server written in Go",
|
||||
"Enjoy your stay on TechNet!"
|
||||
],
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"file": "techircd.log",
|
||||
"max_size": 100,
|
||||
"max_backups": 3,
|
||||
"max_age": 28
|
||||
}
|
||||
}
|
||||
117
configs/opers.conf
Normal file
117
configs/opers.conf
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"rank_names": {
|
||||
"rank_1": "Helper",
|
||||
"rank_2": "Moderator",
|
||||
"rank_3": "Operator",
|
||||
"rank_4": "Administrator",
|
||||
"rank_5": "Owner"
|
||||
},
|
||||
"classes": [
|
||||
{
|
||||
"name": "helper",
|
||||
"rank": 1,
|
||||
"description": "Helper - Basic moderation commands",
|
||||
"permissions": [
|
||||
"kick",
|
||||
"topic",
|
||||
"mode_channel"
|
||||
],
|
||||
"color": "green",
|
||||
"symbol": "%"
|
||||
},
|
||||
{
|
||||
"name": "moderator",
|
||||
"rank": 2,
|
||||
"description": "Moderator - Channel and user management",
|
||||
"permissions": [
|
||||
"ban",
|
||||
"unban",
|
||||
"kick",
|
||||
"mute",
|
||||
"topic",
|
||||
"mode_channel",
|
||||
"mode_user",
|
||||
"who_override"
|
||||
],
|
||||
"inherits": "helper",
|
||||
"color": "blue",
|
||||
"symbol": "@"
|
||||
},
|
||||
{
|
||||
"name": "operator",
|
||||
"rank": 3,
|
||||
"description": "Operator - Server management commands",
|
||||
"permissions": [
|
||||
"kill",
|
||||
"rehash",
|
||||
"connect",
|
||||
"squit",
|
||||
"wallops",
|
||||
"operwall"
|
||||
],
|
||||
"inherits": "moderator",
|
||||
"color": "red",
|
||||
"symbol": "*"
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"rank": 4,
|
||||
"description": "Administrator - Full server control",
|
||||
"permissions": ["*"],
|
||||
"color": "purple",
|
||||
"symbol": "&"
|
||||
},
|
||||
{
|
||||
"name": "owner",
|
||||
"rank": 5,
|
||||
"description": "Server Owner - Ultimate authority",
|
||||
"permissions": [
|
||||
"*",
|
||||
"shutdown",
|
||||
"restart"
|
||||
],
|
||||
"color": "gold",
|
||||
"symbol": "~"
|
||||
}
|
||||
],
|
||||
"opers": [
|
||||
{
|
||||
"name": "admin",
|
||||
"password": "changeme_please",
|
||||
"host": "*@localhost",
|
||||
"class": "admin",
|
||||
"flags": [],
|
||||
"max_clients": 1000,
|
||||
"contact": "admin@example.com",
|
||||
"last_seen": ""
|
||||
},
|
||||
{
|
||||
"name": "helper1",
|
||||
"password": "helper_password",
|
||||
"host": "*@192.168.1.*",
|
||||
"class": "helper",
|
||||
"flags": [],
|
||||
"max_clients": 100,
|
||||
"contact": "helper@example.com",
|
||||
"last_seen": ""
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"require_ssl": false,
|
||||
"max_failed_attempts": 3,
|
||||
"lockout_duration_minutes": 30,
|
||||
"allowed_commands": [
|
||||
"OPER",
|
||||
"KILL",
|
||||
"REHASH",
|
||||
"WALLOPS",
|
||||
"OPERWALL",
|
||||
"CONNECT",
|
||||
"SQUIT"
|
||||
],
|
||||
"log_oper_actions": true,
|
||||
"notify_on_oper_up": true,
|
||||
"auto_expire_inactive_days": 365,
|
||||
"require_two_factor": false
|
||||
}
|
||||
}
|
||||
53
configs/services.json
Normal file
53
configs/services.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"enable": true,
|
||||
"nickserv": {
|
||||
"enable": true,
|
||||
"nick": "NickServ",
|
||||
"user": "services",
|
||||
"host": "services.techircd.net",
|
||||
"realname": "Nickname Registration Service",
|
||||
"expire_time": 30,
|
||||
"identify_timeout": 60,
|
||||
"email_verify": false,
|
||||
"restrict_registration": false
|
||||
},
|
||||
"chanserv": {
|
||||
"enable": true,
|
||||
"nick": "ChanServ",
|
||||
"user": "services",
|
||||
"host": "services.techircd.net",
|
||||
"realname": "Channel Registration Service",
|
||||
"expire_time": 30,
|
||||
"max_channels": 20,
|
||||
"auto_deop": true
|
||||
},
|
||||
"operserv": {
|
||||
"enable": true,
|
||||
"nick": "OperServ",
|
||||
"user": "services",
|
||||
"host": "services.techircd.net",
|
||||
"realname": "Operator Service"
|
||||
},
|
||||
"memoserv": {
|
||||
"enable": true,
|
||||
"nick": "MemoServ",
|
||||
"user": "services",
|
||||
"host": "services.techircd.net",
|
||||
"realname": "Memo Service",
|
||||
"max_memos": 50,
|
||||
"memo_expire": 365
|
||||
},
|
||||
"database": {
|
||||
"enable": true,
|
||||
"type": "mysql",
|
||||
"database": "techircd_services",
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"username": "techircd",
|
||||
"password": "changeme",
|
||||
"options": {
|
||||
"charset": "utf8mb4",
|
||||
"parseTime": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
100
database.go
Normal file
100
database.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Database layer for persistent storage
|
||||
type DatabaseConfig struct {
|
||||
Type string `json:"type"` // sqlite, mysql, postgres
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Options map[string]string `json:"options"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
config DatabaseConfig
|
||||
}
|
||||
|
||||
// User account storage
|
||||
type UserAccount struct {
|
||||
ID int `db:"id"`
|
||||
Nick string `db:"nick"`
|
||||
PasswordHash string `db:"password_hash"`
|
||||
Email string `db:"email"`
|
||||
RegisterTime time.Time `db:"register_time"`
|
||||
LastSeen time.Time `db:"last_seen"`
|
||||
Flags []string `db:"flags"`
|
||||
}
|
||||
|
||||
// Channel registration
|
||||
type RegisteredChannel struct {
|
||||
ID int `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Founder string `db:"founder"`
|
||||
RegisterTime time.Time `db:"register_time"`
|
||||
Topic string `db:"topic"`
|
||||
Modes string `db:"modes"`
|
||||
AccessList []ChannelAccess `db:"-"`
|
||||
}
|
||||
|
||||
type ChannelAccess struct {
|
||||
Nick string `db:"nick"`
|
||||
Level int `db:"level"` // 1=voice, 10=halfop, 50=op, 100=admin, 500=founder
|
||||
}
|
||||
|
||||
// Persistent bans/quiets
|
||||
type NetworkBan struct {
|
||||
ID int `db:"id"`
|
||||
Mask string `db:"mask"`
|
||||
Reason string `db:"reason"`
|
||||
SetBy string `db:"set_by"`
|
||||
SetTime time.Time `db:"set_time"`
|
||||
Duration int `db:"duration"` // 0 = permanent
|
||||
Type string `db:"type"` // gline, kline, zline, qline
|
||||
}
|
||||
|
||||
// Statistics tracking
|
||||
type ServerStats struct {
|
||||
Date time.Time `db:"date"`
|
||||
MaxUsers int `db:"max_users"`
|
||||
MaxChannels int `db:"max_channels"`
|
||||
TotalConnections int64 `db:"total_connections"`
|
||||
MessageCount int64 `db:"message_count"`
|
||||
OperCount int `db:"oper_count"`
|
||||
}
|
||||
|
||||
func NewDatabase(config DatabaseConfig) (*Database, error) {
|
||||
// Initialize database connection
|
||||
return &Database{config: config}, nil
|
||||
}
|
||||
|
||||
func (db *Database) CreateTables() error {
|
||||
// Create all necessary tables
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) GetUserAccount(nick string) (*UserAccount, error) {
|
||||
// Retrieve user account
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *Database) SaveUserAccount(account *UserAccount) error {
|
||||
// Save user account
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) GetChannelAccess(channel string) ([]ChannelAccess, error) {
|
||||
// Get channel access list
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (db *Database) LogStatistics(stats ServerStats) error {
|
||||
// Log daily statistics
|
||||
return nil
|
||||
}
|
||||
32
docs/CHANGELOG.md
Normal file
32
docs/CHANGELOG.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to TechIRCd will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2025-07-30
|
||||
|
||||
### Added
|
||||
- Initial release of TechIRCd
|
||||
- Full IRC protocol implementation (RFC 2812 compliant)
|
||||
- Advanced channel management with operator hierarchy
|
||||
- Extended ban system with quiet mode support (~q:mask)
|
||||
- Comprehensive operator features and SNOmasks
|
||||
- User modes system with SSL detection
|
||||
- Enhanced stability with panic recovery
|
||||
- Real-time health monitoring and metrics
|
||||
- Configuration validation and sanitization
|
||||
- Graceful shutdown capabilities
|
||||
- Flood protection with operator exemption
|
||||
|
||||
### Features
|
||||
- Channel modes: +m +n +t +i +s +p +k +l +b
|
||||
- User modes: +i +w +s +o +x +B +z +r
|
||||
- Operator commands: KILL, GLOBALNOTICE, OPERWALL, WALLOPS, REHASH, TRACE
|
||||
- SNOmasks: +c +k +o +x +f +n +s +d
|
||||
- Extended ban types with quiet mode support
|
||||
- Health monitoring with memory and goroutine tracking
|
||||
- Comprehensive error handling and recovery systems
|
||||
|
||||
[1.0.0]: https://github.com/ComputerTech312/TechIRCd/releases/tag/v1.0.0
|
||||
165
docs/CONNECTION_HANDLING.md
Normal file
165
docs/CONNECTION_HANDLING.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# TechIRCd Connection Handling Improvements
|
||||
|
||||
## Overview
|
||||
TechIRCd now features **robust connection handling** designed to handle real-world network conditions, client behaviors, and edge cases gracefully.
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 🛡️ **Robust Client Connection Handling**
|
||||
|
||||
#### **1. Comprehensive Error Recovery**
|
||||
- **Panic Recovery**: All client handlers have panic recovery with detailed logging
|
||||
- **Graceful Cleanup**: Proper resource cleanup even when connections fail
|
||||
- **Connection Validation**: Validates connections and server state before processing
|
||||
|
||||
#### **2. Smart Timeout Management**
|
||||
- **Registration Timeout**: 60 seconds for initial registration (configurable)
|
||||
- **Read Timeouts**: 5 minutes for registered clients, 30 seconds during registration
|
||||
- **Progressive Timeouts**: More lenient timeouts for new connections
|
||||
|
||||
#### **3. Enhanced Message Processing**
|
||||
- **Binary Data Detection**: Detects and rejects non-text data
|
||||
- **Size Limits**: Truncates oversized messages (512 bytes max)
|
||||
- **Empty Line Handling**: Gracefully ignores empty lines
|
||||
- **Encoding Validation**: Checks for valid IRC characters
|
||||
|
||||
#### **4. Intelligent Flood Protection**
|
||||
- **Registration Leniency**: More generous limits during connection setup
|
||||
- **Sliding Window**: Proper flood detection with time windows
|
||||
- **Configurable Limits**: Respects server configuration settings
|
||||
|
||||
### 🔧 **Server Connection Management**
|
||||
|
||||
#### **1. Connection Limits**
|
||||
- **Max Client Enforcement**: Rejects connections when server is full
|
||||
- **Resource Protection**: Prevents server overload
|
||||
- **Graceful Rejection**: Sends proper error message before closing
|
||||
|
||||
#### **2. Accept Error Handling**
|
||||
- **Temporary Error Recovery**: Handles temporary network errors
|
||||
- **Retry Logic**: Continues accepting after temporary failures
|
||||
- **Error Classification**: Different handling for different error types
|
||||
|
||||
#### **3. Client State Tracking**
|
||||
- **Connection Counting**: Real-time client count tracking
|
||||
- **State Validation**: Ensures clients are properly initialized
|
||||
- **Resource Monitoring**: Tracks and logs connection statistics
|
||||
|
||||
### 📊 **Enhanced Logging and Debugging**
|
||||
|
||||
#### **1. Detailed Connection Logging**
|
||||
```
|
||||
2025/09/03 19:02:44 New client connected from 46.24.193.29:60237 (total: 1)
|
||||
2025/09/03 19:02:44 Starting robust client handler for 46.24.193.29:60237
|
||||
2025/09/03 19:02:44 Client 46.24.193.29:60237: CAP LS 302
|
||||
2025/09/03 19:02:44 Client 46.24.193.29:60237: NICK TestUser
|
||||
2025/09/03 19:02:44 Client registration completed: TestUser (testuser) from 46.24.193.29:60237
|
||||
```
|
||||
|
||||
#### **2. Error Context**
|
||||
- **Connection Source**: Always logs client IP/port
|
||||
- **Error Details**: Specific error messages for troubleshooting
|
||||
- **State Information**: Logs registration status and timing
|
||||
|
||||
#### **3. Performance Metrics**
|
||||
- **Connection Duration**: Tracks how long connections last
|
||||
- **Message Counts**: Monitors message frequency
|
||||
- **Registration Time**: Measures time to complete registration
|
||||
|
||||
### 🔒 **Security Enhancements**
|
||||
|
||||
#### **1. Protocol Validation**
|
||||
- **Command Validation**: Ensures only valid IRC commands are processed
|
||||
- **Data Sanitization**: Removes dangerous or malformed input
|
||||
- **Buffer Management**: Prevents buffer overflow attacks
|
||||
|
||||
#### **2. Connection Security**
|
||||
- **Rate Limiting**: Prevents rapid connection attempts
|
||||
- **Resource Limits**: Protects against resource exhaustion
|
||||
- **Input Validation**: Validates all client input before processing
|
||||
|
||||
### 🚀 **Real-World Compatibility**
|
||||
|
||||
#### **1. IRC Client Support**
|
||||
- **HexChat Compatible**: Tested with popular IRC clients
|
||||
- **IRCv3 Support**: Handles capability negotiation properly
|
||||
- **Standard Compliance**: Follows RFC 2812 standards
|
||||
|
||||
#### **2. Network Conditions**
|
||||
- **Slow Connections**: Handles high-latency connections
|
||||
- **Unstable Networks**: Recovers from temporary network issues
|
||||
- **Mobile Clients**: Optimized for mobile network conditions
|
||||
|
||||
#### **3. Edge Cases**
|
||||
- **Partial Messages**: Handles incomplete message transmission
|
||||
- **Connection Drops**: Graceful handling of sudden disconnections
|
||||
- **Server Restarts**: Proper cleanup during server shutdown
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### **Timeout Settings**
|
||||
```json
|
||||
{
|
||||
"limits": {
|
||||
"registration_timeout": 60,
|
||||
"ping_timeout": 300,
|
||||
"flood_lines": 10,
|
||||
"flood_seconds": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **Connection Limits**
|
||||
```json
|
||||
{
|
||||
"limits": {
|
||||
"max_clients": 1000,
|
||||
"max_channels": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### **For Users**
|
||||
- ✅ **Reliable Connections**: Less likely to drop due to network issues
|
||||
- ✅ **Faster Registration**: Optimized registration process
|
||||
- ✅ **Better Error Messages**: Clear feedback when issues occur
|
||||
- ✅ **Mobile Friendly**: Works well on mobile networks
|
||||
|
||||
### **For Administrators**
|
||||
- ✅ **Detailed Logging**: Easy troubleshooting with comprehensive logs
|
||||
- ✅ **Resource Protection**: Server stays stable under load
|
||||
- ✅ **Security**: Protection against malformed data and attacks
|
||||
- ✅ **Monitoring**: Real-time connection statistics
|
||||
|
||||
### **For Developers**
|
||||
- ✅ **Code Clarity**: Well-structured connection handling code
|
||||
- ✅ **Error Recovery**: Robust error handling prevents crashes
|
||||
- ✅ **Debugging**: Extensive logging for issue resolution
|
||||
- ✅ **Maintainability**: Clean separation of concerns
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### **Basic Connection Test**
|
||||
```bash
|
||||
# Test with telnet
|
||||
telnet your-server.com 6667
|
||||
|
||||
# Send IRC commands
|
||||
NICK TestUser
|
||||
USER testuser 0 * :Test User
|
||||
```
|
||||
|
||||
### **HexChat Configuration**
|
||||
- **Server**: your-server.com
|
||||
- **Port**: 6667 (or 6697 for SSL)
|
||||
- **SSL**: Disabled (unless you have SSL configured)
|
||||
- **Character Set**: UTF-8
|
||||
|
||||
### **Load Testing**
|
||||
- Test with multiple simultaneous connections
|
||||
- Try rapid connect/disconnect cycles
|
||||
- Test with slow network conditions
|
||||
|
||||
TechIRCd now provides **enterprise-grade connection handling** suitable for production IRC networks!
|
||||
65
docs/CONTRIBUTING.md
Normal file
65
docs/CONTRIBUTING.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Contributing to TechIRCd
|
||||
|
||||
We love your input! We want to make contributing to TechIRCd as easy and transparent as possible.
|
||||
|
||||
## Development Process
|
||||
|
||||
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. Issue that pull request!
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow standard Go formatting (`gofmt`)
|
||||
- Use meaningful variable and function names
|
||||
- Add comments for exported functions and complex logic
|
||||
- Keep functions focused and single-purpose
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
go test ./...
|
||||
|
||||
# Run tests with coverage
|
||||
go test -cover ./...
|
||||
|
||||
# Run tests with race detection
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
## Commit Messages
|
||||
|
||||
- Use the present tense ("Add feature" not "Added feature")
|
||||
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||
- Limit the first line to 72 characters or less
|
||||
- Reference issues and pull requests liberally after the first line
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening)
|
||||
|
||||
## Feature Requests
|
||||
|
||||
We welcome feature requests! Please provide:
|
||||
|
||||
- Clear description of the feature
|
||||
- Use case or motivation
|
||||
- Possible implementation approach (if you have ideas)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
||||
174
docs/ENHANCEMENT_ROADMAP.md
Normal file
174
docs/ENHANCEMENT_ROADMAP.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# TechIRCd Enhancement Roadmap
|
||||
|
||||
## 🚀 Phase 1: Core Improvements (High Priority)
|
||||
|
||||
### 1.1 Missing IRC Commands (2-3 weeks)
|
||||
- [ ] USERHOST command
|
||||
- [ ] ISON command
|
||||
- [ ] STATS command with detailed server statistics
|
||||
- [ ] TIME command
|
||||
- [ ] VERSION command
|
||||
- [ ] ADMIN command
|
||||
- [ ] INFO command
|
||||
- [ ] LUSERS command
|
||||
- [ ] Enhanced LIST with pattern matching
|
||||
- [ ] SILENCE command for user-level blocking
|
||||
|
||||
### 1.2 Channel Mode Enhancements (1-2 weeks)
|
||||
- [ ] Ban exceptions (+e mode)
|
||||
- [ ] Invite exceptions (+I mode)
|
||||
- [ ] Permanent channels (+P mode)
|
||||
- [ ] Registered-only channels (+r mode)
|
||||
- [ ] SSL-only channels (+z mode)
|
||||
- [ ] Channel forwarding (+f mode)
|
||||
- [ ] Founder protection mode (~q)
|
||||
- [ ] Admin protection mode (&a)
|
||||
|
||||
### 1.3 Security & Authentication (2-3 weeks)
|
||||
- [ ] SASL authentication support
|
||||
- [ ] Certificate-based authentication
|
||||
- [ ] Rate limiting per IP/user
|
||||
- [ ] GeoIP blocking capabilities
|
||||
- [ ] DDoS protection mechanisms
|
||||
- [ ] Enhanced flood protection
|
||||
- [ ] Connection throttling
|
||||
|
||||
## 🌐 Phase 2: Network & Scaling (Medium Priority)
|
||||
|
||||
### 2.1 Enhanced Server Linking (3-4 weeks)
|
||||
- [ ] Burst optimization for large networks
|
||||
- [ ] Network topology visualization
|
||||
- [ ] Automatic failover and recovery
|
||||
- [ ] Load balancing across servers
|
||||
- [ ] Network-wide channel/user sync
|
||||
- [ ] Cluster management interface
|
||||
- [ ] Health monitoring between servers
|
||||
|
||||
### 2.2 Database Integration (2-3 weeks)
|
||||
- [ ] SQLite/MySQL/PostgreSQL support
|
||||
- [ ] User account persistence
|
||||
- [ ] Channel registration system
|
||||
- [ ] Network-wide ban storage
|
||||
- [ ] Statistics logging
|
||||
- [ ] Audit trail logging
|
||||
- [ ] Data migration tools
|
||||
|
||||
### 2.3 Services Framework (4-5 weeks)
|
||||
- [ ] NickServ implementation
|
||||
- [ ] ChanServ implementation
|
||||
- [ ] OperServ implementation
|
||||
- [ ] MemoServ implementation
|
||||
- [ ] BotServ implementation
|
||||
- [ ] Services API framework
|
||||
- [ ] Plugin system for custom services
|
||||
|
||||
## 📊 Phase 3: Advanced Features (Lower Priority)
|
||||
|
||||
### 3.1 Monitoring & Analytics (3-4 weeks)
|
||||
- [ ] Prometheus metrics integration
|
||||
- [ ] Grafana dashboard templates
|
||||
- [ ] Real-time performance monitoring
|
||||
- [ ] User activity analytics
|
||||
- [ ] Channel analytics
|
||||
- [ ] Alert system with notifications
|
||||
- [ ] Performance profiling tools
|
||||
- [ ] Capacity planning metrics
|
||||
|
||||
### 3.2 Web Interface (4-5 weeks)
|
||||
- [ ] Administrative web panel
|
||||
- [ ] REST API endpoints
|
||||
- [ ] GraphQL query interface
|
||||
- [ ] Real-time dashboard with WebSockets
|
||||
- [ ] Mobile-responsive design
|
||||
- [ ] User management interface
|
||||
- [ ] Channel management interface
|
||||
- [ ] Network topology viewer
|
||||
- [ ] Log viewer and search
|
||||
|
||||
### 3.3 Modern Protocol Support (3-4 weeks)
|
||||
- [ ] IRCv3 capabilities
|
||||
- [ ] Message tags support
|
||||
- [ ] Account tracking
|
||||
- [ ] MONITOR command
|
||||
- [ ] Extended JOIN
|
||||
- [ ] CHGHOST support
|
||||
- [ ] Batch processing
|
||||
- [ ] Server-time capability
|
||||
|
||||
## 🎨 Phase 4: Quality of Life (Ongoing)
|
||||
|
||||
### 4.1 Developer Experience
|
||||
- [ ] Comprehensive test suite (>80% coverage)
|
||||
- [ ] CI/CD pipeline setup
|
||||
- [ ] Automated security scanning
|
||||
- [ ] Performance benchmarking
|
||||
- [ ] Documentation improvements
|
||||
- [ ] Code quality tools integration
|
||||
- [ ] Dependency management
|
||||
|
||||
### 4.2 Deployment & Operations
|
||||
- [ ] Docker containerization
|
||||
- [ ] Kubernetes manifests
|
||||
- [ ] Helm charts
|
||||
- [ ] Configuration management
|
||||
- [ ] Backup and restore tools
|
||||
- [ ] Migration utilities
|
||||
- [ ] Health check endpoints
|
||||
|
||||
### 4.3 Community Features
|
||||
- [ ] Plugin architecture
|
||||
- [ ] Extension API
|
||||
- [ ] Custom command framework
|
||||
- [ ] Event system
|
||||
- [ ] Webhook integrations
|
||||
- [ ] Third-party service connectors
|
||||
|
||||
## 🔧 Technical Debt & Improvements
|
||||
|
||||
### Code Structure
|
||||
- [ ] Proper package structure (remove `package main` everywhere)
|
||||
- [ ] Interface definitions for better testing
|
||||
- [ ] Dependency injection framework
|
||||
- [ ] Configuration validation improvements
|
||||
- [ ] Error handling standardization
|
||||
- [ ] Logging framework upgrade
|
||||
|
||||
### Performance Optimizations
|
||||
- [ ] Connection pooling
|
||||
- [ ] Message batching
|
||||
- [ ] Memory optimization
|
||||
- [ ] CPU profiling and optimization
|
||||
- [ ] Network I/O improvements
|
||||
- [ ] Concurrent processing enhancements
|
||||
|
||||
### Security Hardening
|
||||
- [ ] Input validation improvements
|
||||
- [ ] Buffer overflow protection
|
||||
- [ ] Memory safety audits
|
||||
- [ ] Cryptographic improvements
|
||||
- [ ] Secure defaults
|
||||
- [ ] Vulnerability scanning
|
||||
|
||||
## 📈 Implementation Timeline
|
||||
|
||||
**Total Estimated Time: 6-8 months**
|
||||
|
||||
- **Month 1-2**: Phase 1 (Core Improvements)
|
||||
- **Month 3-4**: Phase 2 (Network & Scaling)
|
||||
- **Month 5-6**: Phase 3 (Advanced Features)
|
||||
- **Month 7-8**: Phase 4 (Quality of Life) + Polish
|
||||
|
||||
## 🎯 Quick Wins (Can implement immediately)
|
||||
|
||||
1. **USERHOST command** (1 day)
|
||||
2. **ISON command** (1 day)
|
||||
3. **TIME command** (1 day)
|
||||
4. **VERSION command** (1 day)
|
||||
5. **Enhanced LIST filtering** (2-3 days)
|
||||
6. **Ban exceptions (+e mode)** (2-3 days)
|
||||
7. **Invite exceptions (+I mode)** (2-3 days)
|
||||
8. **Basic SASL PLAIN** (3-4 days)
|
||||
9. **Prometheus metrics** (3-4 days)
|
||||
10. **Basic web stats API** (2-3 days)
|
||||
|
||||
These improvements would make TechIRCd one of the most feature-complete and modern IRC servers available!
|
||||
176
docs/GOD_MODE_STEALTH.md
Normal file
176
docs/GOD_MODE_STEALTH.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# God Mode and Stealth Mode
|
||||
|
||||
TechIRCd supports two advanced operator features via user modes:
|
||||
|
||||
## God Mode (+G)
|
||||
|
||||
God Mode gives operators ultimate channel override capabilities.
|
||||
|
||||
### Usage
|
||||
```
|
||||
/mode nickname +G # Enable God Mode
|
||||
/mode nickname -G # Disable God Mode
|
||||
```
|
||||
|
||||
### Capabilities
|
||||
- **Channel Access**: Can join any channel regardless of bans, limits, keys, or invite-only status
|
||||
- **Kick Immunity**: Cannot be kicked from channels by anyone, including other operators
|
||||
- **Ban Immunity**: Can join and remain in channels even when banned
|
||||
- **Invite Override**: Can join invite-only channels without being invited
|
||||
- **Limit Override**: Can join channels that have reached their user limit
|
||||
- **Key Override**: Can join channels with passwords/keys without providing them
|
||||
|
||||
### Requirements
|
||||
- Must be an IRC operator (`/oper`)
|
||||
- Must have `god_mode` permission in operator class configuration
|
||||
- Only the user themselves can set/unset their God Mode
|
||||
|
||||
### Security Notes
|
||||
- God Mode actions are logged to operator snomasks (`+o`)
|
||||
- Use responsibly - this bypasses all normal channel protections
|
||||
- Intended for emergency situations and network administration
|
||||
|
||||
## Stealth Mode (+S)
|
||||
|
||||
Stealth Mode makes operators invisible to regular users while remaining visible to other operators.
|
||||
|
||||
### Usage
|
||||
```
|
||||
/mode nickname +S # Enable Stealth Mode
|
||||
/mode nickname -S # Disable Stealth Mode
|
||||
```
|
||||
|
||||
### Effects
|
||||
- **WHO Command**: Stealth users don't appear in `/who` results for regular users
|
||||
- **NAMES Command**: Stealth users don't appear in channel user lists for regular users
|
||||
- **Channel Lists**: Regular users can't see stealth operators in channels
|
||||
- **Operator Visibility**: Other operators can always see stealth users
|
||||
|
||||
### Requirements
|
||||
- Must be an IRC operator (`/oper`)
|
||||
- Must have `stealth_mode` permission in operator class configuration
|
||||
- Only the user themselves can set/unset their Stealth Mode
|
||||
|
||||
### Use Cases
|
||||
- Undercover moderation and monitoring
|
||||
- Reduced operator visibility during investigations
|
||||
- Network administration without user awareness
|
||||
|
||||
## Configuration
|
||||
|
||||
Add permissions to operator classes in `configs/opers.conf`:
|
||||
|
||||
```json
|
||||
{
|
||||
"classes": [
|
||||
{
|
||||
"name": "admin",
|
||||
"rank": 4,
|
||||
"description": "Administrator with special powers",
|
||||
"permissions": [
|
||||
"*",
|
||||
"god_mode",
|
||||
"stealth_mode"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
```
|
||||
# As an operator with god_mode permission:
|
||||
/mode mynick +G
|
||||
# *** GOD MODE enabled - You have ultimate power!
|
||||
|
||||
# Join a banned/invite-only channel:
|
||||
/join #private-channel
|
||||
# Successfully joins despite restrictions
|
||||
|
||||
# Disable God Mode:
|
||||
/mode mynick -G
|
||||
# *** GOD MODE disabled
|
||||
|
||||
# Enable Stealth Mode:
|
||||
/mode mynick +S
|
||||
# *** STEALTH MODE enabled - You are now invisible to users
|
||||
|
||||
# Regular users won't see you in:
|
||||
/who #channel
|
||||
/names #channel
|
||||
|
||||
# Other operators will still see you
|
||||
```
|
||||
|
||||
### Combined Usage
|
||||
```
|
||||
# Enable both modes simultaneously:
|
||||
/mode mynick +GS
|
||||
# *** GOD MODE enabled - You have ultimate power!
|
||||
# *** STEALTH MODE enabled - You are now invisible to users
|
||||
|
||||
# Now you can:
|
||||
# - Join any channel (God Mode)
|
||||
# - Remain invisible to regular users (Stealth Mode)
|
||||
# - Be visible to other operators
|
||||
```
|
||||
|
||||
### Permission Checking
|
||||
```
|
||||
# Attempting without proper permissions:
|
||||
/mode mynick +G
|
||||
# :server 481 mynick :Permission Denied - You need god_mode permission
|
||||
|
||||
# Must be oper first:
|
||||
/mode mynick +S
|
||||
# :server 481 mynick :Permission Denied- You're not an IRC operator
|
||||
```
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### God Mode
|
||||
- Stored as user mode `+G`
|
||||
- Checked via `HasGodMode()` method
|
||||
- Bypasses channel restrictions in:
|
||||
- `JOIN` command (bans, limits, keys, invite-only)
|
||||
- `KICK` command (cannot be kicked)
|
||||
- Channel access validation
|
||||
|
||||
### Stealth Mode
|
||||
- Stored as user mode `+S`
|
||||
- Checked via `HasStealthMode()` method
|
||||
- Filtered in:
|
||||
- `WHO` command responses
|
||||
- `NAMES` command responses
|
||||
- Channel member visibility
|
||||
|
||||
### Mode Persistence
|
||||
- User modes are stored per-client session
|
||||
- Lost on disconnect/reconnect
|
||||
- Must be re-enabled after each connection
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Audit Trail**: All God Mode and Stealth Mode activations are logged
|
||||
2. **Permission Based**: Requires explicit operator class permissions
|
||||
3. **Self-Only**: Users can only set modes on themselves
|
||||
4. **Operator Level**: Requires existing operator privileges
|
||||
5. **Reversible**: Can be disabled at any time
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Mode Not Setting
|
||||
- Verify you are opered (`/oper`)
|
||||
- Check operator class has required permissions
|
||||
- Ensure using correct syntax (`/mode nickname +G`)
|
||||
|
||||
### Not Working as Expected
|
||||
- God Mode only affects channel restrictions, not other commands
|
||||
- Stealth Mode only hides from regular users, not operators
|
||||
- Modes are case-sensitive (`+G` not `+g`)
|
||||
|
||||
### Permission Denied
|
||||
- Contact network administrator to add permissions to your operator class
|
||||
- Verify operator class configuration in `configs/opers.conf`
|
||||
122
docs/IRCV3_FEATURES.md
Normal file
122
docs/IRCV3_FEATURES.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# TechIRCd IRCv3 Features
|
||||
|
||||
## ✅ Implemented IRCv3 Features
|
||||
|
||||
### Core Capability Negotiation
|
||||
- **CAP LS** - List available capabilities
|
||||
- **CAP LIST** - List enabled capabilities
|
||||
- **CAP REQ** - Request capabilities
|
||||
- **CAP ACK/NAK** - Acknowledge/reject capability requests
|
||||
- **CAP END** - End capability negotiation
|
||||
|
||||
### Supported Capabilities
|
||||
|
||||
#### IRCv3.1 Base
|
||||
- ✅ **sasl** - SASL authentication (PLAIN mechanism)
|
||||
- ✅ **multi-prefix** - Multiple channel prefixes (~@%+)
|
||||
- ✅ **away-notify** - Away state change notifications
|
||||
- ✅ **account-notify** - Account login/logout notifications
|
||||
- ✅ **extended-join** - JOIN with account and real name
|
||||
|
||||
#### IRCv3.2 Extensions
|
||||
- ✅ **server-time** - Timestamp tags on messages
|
||||
- ✅ **userhost-in-names** - Full hostmasks in NAMES
|
||||
- ✅ **monitor-notify** - MONITOR command for presence
|
||||
- ✅ **account-tag** - Account tags on messages
|
||||
- ✅ **message-tags** - IRCv3 message tag framework
|
||||
|
||||
#### IRCv3.3 Features
|
||||
- ✅ **echo-message** - Echo sent messages back to sender
|
||||
- ✅ **chghost** - Host change notifications
|
||||
- ✅ **invite-notify** - Channel invite notifications
|
||||
- ✅ **batch** - Message batching support
|
||||
|
||||
## 🚀 Usage Examples
|
||||
|
||||
### Basic Capability Negotiation
|
||||
```
|
||||
Client: CAP LS 302
|
||||
Server: :server CAP * LS :account-notify away-notify extended-join multi-prefix sasl server-time userhost-in-names account-tag message-tags echo-message batch chghost invite-notify monitor-notify
|
||||
|
||||
Client: CAP REQ :server-time message-tags sasl
|
||||
Server: :server CAP * ACK :server-time message-tags sasl
|
||||
|
||||
Client: CAP END
|
||||
```
|
||||
|
||||
### SASL Authentication
|
||||
```
|
||||
Client: CAP REQ :sasl
|
||||
Server: :server CAP * ACK :sasl
|
||||
Client: AUTHENTICATE PLAIN
|
||||
Server: AUTHENTICATE +
|
||||
Client: AUTHENTICATE dXNlcm5hbWU6cGFzc3dvcmQ=
|
||||
Server: :server 903 * :SASL authentication successful
|
||||
Client: CAP END
|
||||
```
|
||||
|
||||
### Server-Time Tags
|
||||
```
|
||||
@time=2025-08-28T17:01:49.123Z :nick!user@host PRIVMSG #channel :Hello!
|
||||
```
|
||||
|
||||
### Account Tags
|
||||
```
|
||||
@account=alice;time=2025-08-28T17:01:49.123Z :alice!user@host PRIVMSG #channel :Hello!
|
||||
```
|
||||
|
||||
### Extended JOIN
|
||||
```
|
||||
:nick!user@host JOIN #channel alice :Real Name
|
||||
```
|
||||
|
||||
### MONITOR Command
|
||||
```
|
||||
Client: MONITOR + alice,bob,charlie
|
||||
Server: :server 730 nick :alice,bob
|
||||
Server: :server 731 nick :charlie
|
||||
|
||||
Client: MONITOR L
|
||||
Server: :server 732 nick :alice,bob,charlie
|
||||
Server: :server 733 nick :End of MONITOR list
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
IRCv3 features are automatically available when clients request them through CAP negotiation. No special server configuration required.
|
||||
|
||||
### SASL Accounts
|
||||
Currently supports simple username/password authentication. Edit the `authenticateUser()` function in `commands.go` to integrate with your authentication system.
|
||||
|
||||
## 🎯 Advanced Features
|
||||
|
||||
### Message Tags
|
||||
TechIRCd automatically adds appropriate tags based on client capabilities:
|
||||
- `time` - Server timestamp (if server-time enabled)
|
||||
- `account` - User account name (if logged in and account-tag enabled)
|
||||
|
||||
### Multi-Prefix Support
|
||||
Shows all user channel modes: `~@%+nick` for Owner/Op/Halfop/Voice
|
||||
|
||||
### Echo Message
|
||||
When enabled, clients receive copies of their own messages, useful for multi-device synchronization.
|
||||
|
||||
## 🔮 Future IRCv3 Extensions
|
||||
|
||||
Planned for future releases:
|
||||
- **draft/chathistory** - Message history replay
|
||||
- **draft/resume** - Connection state resumption
|
||||
- **labeled-response** - Request/response correlation
|
||||
- **standard-replies** - Standardized error responses
|
||||
|
||||
## 💡 Client Compatibility
|
||||
|
||||
TechIRCd's IRCv3 implementation is compatible with:
|
||||
- **HexChat** - Full feature support
|
||||
- **IRCCloud** - Full feature support
|
||||
- **Textual** - Full feature support
|
||||
- **WeeChat** - Full feature support
|
||||
- **KiwiIRC** - Full feature support
|
||||
- **IRCv3-compliant bots** - Full API support
|
||||
|
||||
Legacy IRC clients without IRCv3 support continue to work normally without any IRCv3 features.
|
||||
290
docs/LINKING.md
Normal file
290
docs/LINKING.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# TechIRCd Server Linking
|
||||
|
||||
TechIRCd now supports IRC server linking, allowing multiple IRC servers to form a network. This enables users on different servers to communicate with each other seamlessly.
|
||||
|
||||
## Features
|
||||
|
||||
- **Server-to-Server Communication**: Full IRC protocol compliance for server linking
|
||||
- **Hub and Leaf Configuration**: Support for hub/leaf network topologies
|
||||
- **Auto-Connect**: Automatic connection to configured servers on startup
|
||||
- **Operator Commands**: Manual connection and disconnection controls
|
||||
- **Security**: Password-based authentication for server connections
|
||||
- **Network Transparency**: Users can see and communicate across the entire network
|
||||
|
||||
## Configuration
|
||||
|
||||
Server linking is configured in the `config.json` file under the `linking` section:
|
||||
|
||||
```json
|
||||
{
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"server_port": 6697,
|
||||
"password": "linkpassword123",
|
||||
"hub": false,
|
||||
"auto_connect": false,
|
||||
"links": [
|
||||
{
|
||||
"name": "hub.technet.org",
|
||||
"host": "127.0.0.1",
|
||||
"port": 6697,
|
||||
"password": "linkpassword123",
|
||||
"auto_connect": false,
|
||||
"hub": true,
|
||||
"description": "TechNet Hub Server"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- **enable**: Enable or disable server linking functionality
|
||||
- **server_port**: Port to listen on for incoming server connections
|
||||
- **password**: Default password for server authentication
|
||||
- **hub**: Whether this server acts as a hub (can connect to multiple servers)
|
||||
- **auto_connect**: Automatically attempt to connect to configured links on startup
|
||||
- **links**: Array of server configurations to link with
|
||||
|
||||
### Link Configuration
|
||||
|
||||
Each link in the `links` array supports:
|
||||
|
||||
- **name**: Unique server name (should match the remote server's configured name)
|
||||
- **host**: Hostname or IP address of the remote server
|
||||
- **port**: Port number the remote server is listening on for server connections
|
||||
- **password**: Authentication password (must match on both servers)
|
||||
- **auto_connect**: Whether to automatically connect to this server
|
||||
- **hub**: Whether the remote server is a hub server
|
||||
- **description**: Human-readable description of the server
|
||||
|
||||
## Operator Commands
|
||||
|
||||
Operators can manually control server links using these commands:
|
||||
|
||||
### CONNECT
|
||||
Connect to a configured server:
|
||||
```
|
||||
/CONNECT <servername> <port> [host]
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
/CONNECT hub.technet.org 6697
|
||||
/CONNECT hub.technet.org 6697 192.168.1.100
|
||||
```
|
||||
|
||||
### SQUIT
|
||||
Disconnect from a linked server:
|
||||
```
|
||||
/SQUIT <servername> [reason]
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
/SQUIT hub.technet.org
|
||||
/SQUIT hub.technet.org :Maintenance required
|
||||
```
|
||||
|
||||
### LINKS
|
||||
Show all connected servers:
|
||||
```
|
||||
/LINKS
|
||||
```
|
||||
|
||||
This displays the network topology showing all connected servers.
|
||||
|
||||
## Network Topology
|
||||
|
||||
TechIRCd supports both hub-and-leaf and mesh network topologies:
|
||||
|
||||
### Hub-and-Leaf
|
||||
```
|
||||
[Hub Server]
|
||||
/ | \
|
||||
[Leaf1] [Leaf2] [Leaf3]
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
- The hub server (`hub: true`) connects to multiple leaf servers
|
||||
- Leaf servers (`hub: false`) only connect to the hub
|
||||
- All inter-server communication routes through the hub
|
||||
|
||||
### Mesh Network
|
||||
```
|
||||
[Server1] --- [Server2]
|
||||
| \ / |
|
||||
| \ / |
|
||||
[Server3] --- [Server4]
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
- All servers can connect to multiple other servers
|
||||
- Provides redundancy and multiple communication paths
|
||||
- More complex but more resilient
|
||||
|
||||
## Server-to-Server Protocol
|
||||
|
||||
TechIRCd implements standard IRC server-to-server protocol commands:
|
||||
|
||||
- **PASS**: Authentication password
|
||||
- **SERVER**: Server introduction and information
|
||||
- **PING/PONG**: Keep-alive and lag detection
|
||||
- **SQUIT**: Server quit/disconnect
|
||||
- **NICK**: User nickname propagation
|
||||
- **USER**: User information propagation
|
||||
- **JOIN/PART**: Channel membership changes
|
||||
- **PRIVMSG/NOTICE**: Message routing between servers
|
||||
- **QUIT**: User disconnection notifications
|
||||
|
||||
## Message Routing
|
||||
|
||||
When servers are linked, messages are automatically routed across the network:
|
||||
|
||||
1. **User Messages**: Private messages and notices are routed to the correct server
|
||||
2. **Channel Messages**: Broadcast to all servers with users in the channel
|
||||
3. **User Lists**: WHO, WHOIS, and NAMES commands work across the network
|
||||
4. **Channel Lists**: LIST shows channels from all linked servers
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Passwords**: Always use strong, unique passwords for server links
|
||||
- **Network Access**: Restrict server linking ports to trusted networks
|
||||
- **Operator Permissions**: Only trusted operators should have CONNECT/SQUIT privileges
|
||||
- **Link Validation**: Server names and passwords are validated before connection
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
1. **Check Configuration**: Ensure server names, hosts, ports, and passwords match
|
||||
2. **Network Connectivity**: Verify network connectivity between servers
|
||||
3. **Firewall**: Ensure server linking ports are open
|
||||
4. **Logs**: Check server logs for connection errors and authentication failures
|
||||
|
||||
### Common Error Messages
|
||||
|
||||
- `No link configuration found`: Server not configured in links array
|
||||
- `Server already connected`: Attempted to connect to already linked server
|
||||
- `Authentication failed`: Password mismatch between servers
|
||||
- `Connection refused`: Network connectivity or firewall issues
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable debug logging to see detailed server linking information:
|
||||
|
||||
```json
|
||||
{
|
||||
"logging": {
|
||||
"level": "debug"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Network Setup
|
||||
|
||||
Here's an example of setting up a 3-server network:
|
||||
|
||||
### Hub Server (hub.technet.org)
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"name": "hub.technet.org"
|
||||
},
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"server_port": 6697,
|
||||
"hub": true,
|
||||
"links": [
|
||||
{
|
||||
"name": "leaf1.technet.org",
|
||||
"host": "192.168.1.101",
|
||||
"port": 6697,
|
||||
"password": "linkpass123",
|
||||
"auto_connect": true,
|
||||
"hub": false
|
||||
},
|
||||
{
|
||||
"name": "leaf2.technet.org",
|
||||
"host": "192.168.1.102",
|
||||
"port": 6697,
|
||||
"password": "linkpass123",
|
||||
"auto_connect": true,
|
||||
"hub": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Leaf Server 1 (leaf1.technet.org)
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"name": "leaf1.technet.org"
|
||||
},
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"server_port": 6697,
|
||||
"hub": false,
|
||||
"links": [
|
||||
{
|
||||
"name": "hub.technet.org",
|
||||
"host": "192.168.1.100",
|
||||
"port": 6697,
|
||||
"password": "linkpass123",
|
||||
"auto_connect": true,
|
||||
"hub": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Leaf Server 2 (leaf2.technet.org)
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"name": "leaf2.technet.org"
|
||||
},
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"server_port": 6697,
|
||||
"hub": false,
|
||||
"links": [
|
||||
{
|
||||
"name": "hub.technet.org",
|
||||
"host": "192.168.1.100",
|
||||
"port": 6697,
|
||||
"password": "linkpass123",
|
||||
"auto_connect": true,
|
||||
"hub": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This creates a hub-and-leaf network where:
|
||||
- The hub automatically connects to both leaf servers
|
||||
- Leaf servers connect only to the hub
|
||||
- Users on any server can communicate with users on other servers
|
||||
- Channels span across all servers in the network
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Network Latency**: High latency between servers may affect user experience
|
||||
- **Bandwidth**: Server linking uses additional bandwidth for message propagation
|
||||
- **CPU Usage**: Message routing and protocol handling requires CPU resources
|
||||
- **Memory Usage**: Additional memory is needed to track remote users and channels
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned improvements for server linking include:
|
||||
|
||||
- **Services Integration**: Support for network services (NickServ, ChanServ, etc.)
|
||||
- **Channel Bursting**: Optimized channel synchronization
|
||||
- **Network Statistics**: Detailed network topology and performance metrics
|
||||
- **Load Balancing**: Intelligent routing for optimal performance
|
||||
- **Redundancy**: Automatic failover and connection recovery
|
||||
355
docs/OPERATOR_SYSTEM.md
Normal file
355
docs/OPERATOR_SYSTEM.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Operator Configuration Guide
|
||||
|
||||
TechIRCd features a sophisticated hierarchical operator system with separate configuration files and granular permission control.
|
||||
|
||||
## Overview
|
||||
|
||||
The operator system consists of:
|
||||
- **Operator Classes**: Define ranks, permissions, and inheritance
|
||||
- **Individual Operators**: Specific users with assigned classes
|
||||
- **Permission System**: Granular control over what operators can do
|
||||
- **Rank Hierarchy**: Higher ranks can operate on lower ranks
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Main Config (`config.json`)
|
||||
```json
|
||||
"oper_config": {
|
||||
"config_file": "configs/opers.conf",
|
||||
"enable": true
|
||||
}
|
||||
```
|
||||
|
||||
### Operator Config (`configs/opers.conf`)
|
||||
Contains the complete operator hierarchy and individual operator definitions.
|
||||
|
||||
## Operator Classes
|
||||
|
||||
### Customizable Rank Names
|
||||
|
||||
TechIRCd allows you to completely customize the names of operator ranks. Instead of using the standard Helper/Moderator/Operator names, you can create themed naming schemes:
|
||||
|
||||
```json
|
||||
"rank_names": {
|
||||
"rank_1": "Cadet", // Instead of "Helper"
|
||||
"rank_2": "Sergeant", // Instead of "Moderator"
|
||||
"rank_3": "Lieutenant", // Instead of "Operator"
|
||||
"rank_4": "Captain", // Instead of "Administrator"
|
||||
"rank_5": "General", // Instead of "Owner"
|
||||
"custom_ranks": {
|
||||
"Field Marshal": 6, // Custom ranks beyond 5
|
||||
"Supreme Commander": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Popular Naming Themes
|
||||
|
||||
#### Gaming/Military Theme
|
||||
- Rank 1: `Cadet`, `Recruit`, `Private`
|
||||
- Rank 2: `Sergeant`, `Corporal`, `Specialist`
|
||||
- Rank 3: `Lieutenant`, `Captain`, `Major`
|
||||
- Rank 4: `Colonel`, `General`, `Commander`
|
||||
- Rank 5: `Admiral`, `Marshal`, `Supreme Commander`
|
||||
|
||||
#### Corporate/Business Theme
|
||||
- Rank 1: `Intern`, `Assistant`, `Associate`
|
||||
- Rank 2: `Specialist`, `Senior Associate`, `Team Lead`
|
||||
- Rank 3: `Manager`, `Senior Manager`, `Department Head`
|
||||
- Rank 4: `Director`, `VP`, `Executive`
|
||||
- Rank 5: `CEO`, `President`, `Chairman`
|
||||
|
||||
#### Fantasy/Medieval Theme
|
||||
- Rank 1: `Apprentice`, `Squire`, `Page`
|
||||
- Rank 2: `Guardian`, `Warrior`, `Mage`
|
||||
- Rank 3: `Knight`, `Paladin`, `Wizard`
|
||||
- Rank 4: `Lord`, `Baron`, `Archmage`
|
||||
- Rank 5: `King`, `Emperor`, `Divine Ruler`
|
||||
|
||||
#### Sci-Fi Theme
|
||||
- Rank 1: `Ensign`, `Technician`, `Cadet`
|
||||
- Rank 2: `Engineer`, `Pilot`, `Specialist`
|
||||
- Rank 3: `Commander`, `Captain`, `Leader`
|
||||
- Rank 4: `Admiral`, `Fleet Commander`, `Director`
|
||||
- Rank 5: `Supreme Admiral`, `Galactic Emperor`, `AI Overlord`
|
||||
|
||||
### Built-in Classes (Default Names)
|
||||
|
||||
#### Helper (Rank 1)
|
||||
- **Symbol**: `%`
|
||||
- **Color**: Green
|
||||
- **Permissions**: Basic moderation
|
||||
- `kick` - Kick users from channels
|
||||
- `topic` - Change channel topics
|
||||
- `mode_channel` - Change channel modes
|
||||
|
||||
#### Moderator (Rank 2)
|
||||
- **Symbol**: `@`
|
||||
- **Color**: Blue
|
||||
- **Inherits**: Helper permissions
|
||||
- **Additional Permissions**:
|
||||
- `ban` / `unban` - Manage channel bans
|
||||
- `mute` - Mute users
|
||||
- `mode_user` - Change user modes
|
||||
- `who_override` - See hidden users in WHO
|
||||
|
||||
#### Operator (Rank 3)
|
||||
- **Symbol**: `*`
|
||||
- **Color**: Red
|
||||
- **Inherits**: Moderator permissions
|
||||
- **Additional Permissions**:
|
||||
- `kill` - Kill user connections
|
||||
- `gline` - Global bans
|
||||
- `rehash` - Reload configuration
|
||||
- `connect` / `squit` - Server linking
|
||||
- `wallops` / `operwall` - Send operator messages
|
||||
|
||||
#### Administrator (Rank 4)
|
||||
- **Symbol**: `&`
|
||||
- **Color**: Purple
|
||||
- **Permissions**: `*` (All permissions)
|
||||
|
||||
#### Owner (Rank 5)
|
||||
- **Symbol**: `~`
|
||||
- **Color**: Gold
|
||||
- **Special Permissions**:
|
||||
- `*` - All permissions
|
||||
- `override_rank` - Can operate on same/higher ranks
|
||||
- `shutdown` / `restart` - Server control
|
||||
|
||||
### Custom Classes
|
||||
|
||||
Create your own operator classes:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "security",
|
||||
"rank": 3,
|
||||
"description": "Security Officer - Network protection",
|
||||
"permissions": [
|
||||
"kill",
|
||||
"gline",
|
||||
"scan_network",
|
||||
"access_logs"
|
||||
],
|
||||
"inherits": "moderator",
|
||||
"color": "orange",
|
||||
"symbol": "!"
|
||||
}
|
||||
```
|
||||
|
||||
## Individual Operators
|
||||
|
||||
### Basic Operator Definition
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "alice",
|
||||
"password": "secure_password_here",
|
||||
"host": "*@trusted.example.com",
|
||||
"class": "moderator",
|
||||
"flags": ["extra_channels"],
|
||||
"max_clients": 500,
|
||||
"contact": "alice@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "bob",
|
||||
"password": "another_secure_password",
|
||||
"host": "admin@192.168.1.*",
|
||||
"class": "admin",
|
||||
"flags": ["debug_access", "special_channels"],
|
||||
"max_clients": 1000,
|
||||
"expires": "2025-12-31",
|
||||
"contact": "bob@company.com",
|
||||
"last_seen": "2025-07-30T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Permission System
|
||||
|
||||
### Standard Permissions
|
||||
|
||||
#### Channel Management
|
||||
- `kick` - Kick users from channels
|
||||
- `ban` / `unban` - Manage channel bans
|
||||
- `topic` - Change channel topics
|
||||
- `mode_channel` - Change channel modes
|
||||
- `mode_user` - Change user modes
|
||||
|
||||
#### User Management
|
||||
- `kill` - Disconnect users
|
||||
- `gline` - Global network bans
|
||||
- `mute` - Silence users
|
||||
- `who_override` - See hidden information
|
||||
|
||||
#### Server Management
|
||||
- `rehash` - Reload configuration
|
||||
- `connect` / `squit` - Server linking
|
||||
- `wallops` / `operwall` - Operator communications
|
||||
- `shutdown` / `restart` - Server control
|
||||
|
||||
#### Special Permissions
|
||||
- `*` - All permissions (wildcard)
|
||||
- `override_rank` - Ignore rank restrictions
|
||||
- `debug_access` - Debug commands
|
||||
- `log_access` - View server logs
|
||||
|
||||
### Custom Permissions
|
||||
|
||||
Add your own permissions for custom commands:
|
||||
|
||||
```json
|
||||
"permissions": [
|
||||
"custom_report",
|
||||
"special_database_access",
|
||||
"network_monitoring"
|
||||
]
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### Settings Configuration
|
||||
|
||||
```json
|
||||
"settings": {
|
||||
"require_ssl": true,
|
||||
"max_failed_attempts": 3,
|
||||
"lockout_duration_minutes": 30,
|
||||
"log_oper_actions": true,
|
||||
"notify_on_oper_up": true,
|
||||
"auto_expire_inactive_days": 365,
|
||||
"require_two_factor": false
|
||||
}
|
||||
```
|
||||
|
||||
### Security Options
|
||||
|
||||
- **SSL Requirement**: Force SSL for operator authentication
|
||||
- **Failed Attempt Tracking**: Lock accounts after failed attempts
|
||||
- **Action Logging**: Log all operator actions
|
||||
- **Auto-Expiration**: Remove inactive operators
|
||||
- **Two-Factor Authentication**: (Future feature)
|
||||
|
||||
## Rank System
|
||||
|
||||
### How Ranks Work
|
||||
|
||||
- **Higher numbers = Higher authority**
|
||||
- **Same rank cannot operate on each other**
|
||||
- **Override permission bypasses rank restrictions**
|
||||
|
||||
### Example Hierarchy
|
||||
|
||||
```
|
||||
Owner (5) -> Can do anything to anyone
|
||||
Admin (4) -> Can operate on Operator (3) and below
|
||||
Operator (3) -> Can operate on Moderator (2) and below
|
||||
Moderator (2) -> Can operate on Helper (1) and below
|
||||
Helper (1) -> Can only operate on regular users
|
||||
```
|
||||
|
||||
## WHOIS Integration
|
||||
|
||||
The operator system integrates with WHOIS to show:
|
||||
|
||||
- Operator status with custom symbols
|
||||
- Class names and descriptions
|
||||
- **Custom rank names** based on your configuration
|
||||
- Permission levels
|
||||
|
||||
Example WHOIS output with custom rank names:
|
||||
```
|
||||
[313] alice is an IRC operator (moderator - Channel and user management) [Sergeant]
|
||||
[313] bob is an IRC operator (captain - Senior officer with full authority) [Captain]
|
||||
```
|
||||
|
||||
With fantasy theme:
|
||||
```
|
||||
[313] merlin is an IRC operator (lord - Ruler of vast territories) [Lord]
|
||||
```
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### Quick Setup - Gaming Theme
|
||||
|
||||
```json
|
||||
{
|
||||
"rank_names": {
|
||||
"rank_1": "Cadet",
|
||||
"rank_2": "Sergeant",
|
||||
"rank_3": "Lieutenant",
|
||||
"rank_4": "Captain",
|
||||
"rank_5": "General"
|
||||
},
|
||||
"classes": [
|
||||
{
|
||||
"name": "cadet",
|
||||
"rank": 1,
|
||||
"description": "New recruit with basic training",
|
||||
"permissions": ["kick", "topic"],
|
||||
"symbol": "+",
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See `/configs/examples/` for complete themed configurations:
|
||||
- `opers-gaming-theme.conf` - Military/Gaming ranks
|
||||
- `opers-corporate-theme.conf` - Business hierarchy
|
||||
- `opers-fantasy-theme.conf` - Medieval/Fantasy theme
|
||||
|
||||
## Migration from Legacy
|
||||
|
||||
TechIRCd automatically falls back to the legacy operator system if:
|
||||
- `oper_config.enable` is `false`
|
||||
- The opers.conf file cannot be loaded
|
||||
- No matching operator is found in the new system
|
||||
|
||||
Legacy operators get basic permissions and rank 1.
|
||||
|
||||
## Commands for Operators
|
||||
|
||||
### Checking Permissions
|
||||
```
|
||||
/WHOIS operator_name # See operator class and rank
|
||||
/OPERLIST # List all operators (future)
|
||||
/OPERHELP # Show operator commands (future)
|
||||
```
|
||||
|
||||
### Management Commands
|
||||
```
|
||||
/OPER name password # Become an operator
|
||||
/OPERWALL message # Send message to all operators
|
||||
/REHASH # Reload configuration
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security
|
||||
1. Use strong passwords for all operators
|
||||
2. Restrict host masks to trusted IPs
|
||||
3. Enable SSL requirement for sensitive ranks
|
||||
4. Regularly audit operator lists
|
||||
5. Set expiration dates for temporary operators
|
||||
|
||||
### Organization
|
||||
1. Create classes that match your network structure
|
||||
2. Use inheritance to avoid permission duplication
|
||||
3. Document custom permissions clearly
|
||||
4. Use descriptive class names and descriptions
|
||||
5. Assign appropriate symbols and colors
|
||||
|
||||
### Maintenance
|
||||
1. Review operator activity regularly
|
||||
2. Remove inactive operators
|
||||
3. Update permissions as needed
|
||||
4. Monitor operator actions through logs
|
||||
5. Keep opers.conf in version control
|
||||
|
||||
This operator system makes TechIRCd extremely flexible for networks of any size, from small communities to large networks with complex hierarchies!
|
||||
161
docs/PROJECT_STRUCTURE.md
Normal file
161
docs/PROJECT_STRUCTURE.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# TechIRCd Enhanced Project Structure
|
||||
|
||||
## Current Structure Issues
|
||||
- All packages use `package main` instead of proper names
|
||||
- Missing comprehensive test coverage
|
||||
- No CI/CD pipeline
|
||||
- Limited documentation
|
||||
- No deployment configurations
|
||||
|
||||
## Proposed Enhanced Structure
|
||||
|
||||
```
|
||||
```
|
||||
TechIRCd/
|
||||
├── .github/ # GitHub-specific files
|
||||
│ ├── workflows/ # GitHub Actions CI/CD
|
||||
│ │ ├── ci.yml # Continuous Integration
|
||||
│ │ ├── release.yml # Automated releases
|
||||
│ │ └── security.yml # Security scanning
|
||||
│ ├── ISSUE_TEMPLATE/ # Issue templates
|
||||
│ │ ├── bug_report.md
|
||||
│ │ ├── feature_request.md
|
||||
│ │ └── question.md
|
||||
│ └── PULL_REQUEST_TEMPLATE.md
|
||||
├── api/ # API definitions (if adding REST API)
|
||||
│ └── v1/
|
||||
│ └── openapi.yaml
|
||||
├── build/ # Build configurations
|
||||
│ └── ci/
|
||||
│ └── scripts/
|
||||
├── cmd/ # Main applications
|
||||
│ ├── techircd/ # Main server
|
||||
│ │ └── main.go
|
||||
│ ├── techircd-admin/ # Admin tool
|
||||
│ │ └── main.go
|
||||
│ └── techircd-client/ # Test client
|
||||
│ └── main.go
|
||||
├── configs/ # Configuration files
|
||||
│ ├── config.json # Default config
|
||||
│ ├── config.prod.json # Production config
|
||||
│ └── config.dev.json # Development config
|
||||
├── deployments/ # Deployment configurations
|
||||
│ ├── kubernetes/
|
||||
│ │ ├── namespace.yaml
|
||||
│ │ ├── deployment.yaml
|
||||
│ │ └── service.yaml
|
||||
│ └── systemd/
|
||||
│ └── techircd.service
|
||||
```
|
||||
├── docs/ # Documentation
|
||||
│ ├── api/ # API documentation
|
||||
│ ├── admin/ # Administrator guide
|
||||
│ ├── user/ # User guide
|
||||
│ ├── development/ # Development guide
|
||||
│ └── examples/ # Usage examples
|
||||
├── examples/ # Example configurations
|
||||
│ ├── simple/
|
||||
│ ├── production/
|
||||
│ └── cluster/
|
||||
├── internal/ # Private application code
|
||||
│ ├── channel/
|
||||
│ │ ├── channel.go
|
||||
│ │ ├── channel_test.go
|
||||
│ │ ├── modes.go
|
||||
│ │ └── permissions.go
|
||||
│ ├── client/
|
||||
│ │ ├── client.go
|
||||
│ │ ├── client_test.go
|
||||
│ │ ├── auth.go
|
||||
│ │ └── connection.go
|
||||
│ ├── commands/
|
||||
│ │ ├── commands.go
|
||||
│ │ ├── commands_test.go
|
||||
│ │ ├── irc.go
|
||||
│ │ └── operator.go
|
||||
│ ├── config/
|
||||
│ │ ├── config.go
|
||||
│ │ ├── config_test.go
|
||||
│ │ └── validation.go
|
||||
│ ├── database/ # Database layer (future)
|
||||
│ │ ├── models/
|
||||
│ │ └── migrations/
|
||||
│ ├── health/
|
||||
│ │ ├── health.go
|
||||
│ │ ├── health_test.go
|
||||
│ │ └── metrics.go
|
||||
│ ├── protocol/ # IRC protocol handling
|
||||
│ │ ├── parser.go
|
||||
│ │ ├── parser_test.go
|
||||
│ │ └── numerics.go
|
||||
│ ├── security/ # Security features
|
||||
│ │ ├── auth.go
|
||||
│ │ ├── ratelimit.go
|
||||
│ │ └── validation.go
|
||||
│ └── server/
|
||||
│ ├── server.go
|
||||
│ ├── server_test.go
|
||||
│ └── handlers.go
|
||||
├── pkg/ # Public library code
|
||||
│ └── irc/ # IRC utilities for external use
|
||||
│ ├── client/
|
||||
│ │ └── client.go
|
||||
│ └── protocol/
|
||||
│ └── constants.go
|
||||
├── scripts/ # Build and utility scripts
|
||||
│ ├── build.sh
|
||||
│ ├── test.sh
|
||||
│ ├── lint.sh
|
||||
│ └── release.sh
|
||||
├── test/ # Test data and utilities
|
||||
│ ├── fixtures/ # Test data
|
||||
│ ├── integration/ # Integration tests
|
||||
│ │ └── server_test.go
|
||||
│ ├── e2e/ # End-to-end tests
|
||||
│ └── performance/ # Performance tests
|
||||
├── tools/ # Supporting tools
|
||||
│ └── migrate/ # Database migration tool
|
||||
├── web/ # Web interface (future)
|
||||
│ ├── static/
|
||||
│ └── templates/
|
||||
├── .editorconfig
|
||||
├── .golangci.yml # Linter configuration
|
||||
├── CHANGELOG.md
|
||||
├── CODE_OF_CONDUCT.md
|
||||
├── CONTRIBUTING.md
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── LICENSE
|
||||
├── Makefile
|
||||
├── README.md
|
||||
└── SECURITY.md
|
||||
```
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1: Core Structure
|
||||
1. Fix package declarations
|
||||
2. Add proper test files
|
||||
3. Create CI/CD pipeline
|
||||
4. Add linting configuration
|
||||
|
||||
### Phase 2: Enhanced Features
|
||||
1. Create admin tools
|
||||
2. Add API endpoints
|
||||
3. Implement database layer
|
||||
4. Add monitoring tools
|
||||
|
||||
### Phase 3: Production Ready
|
||||
1. Add monitoring
|
||||
2. Create deployment configs
|
||||
3. Add security scanning
|
||||
4. Performance optimization
|
||||
|
||||
## Benefits of This Structure
|
||||
|
||||
1. **Professional**: Follows Go and open-source best practices
|
||||
2. **Scalable**: Easy to add new features and maintain
|
||||
3. **Testable**: Comprehensive testing at all levels
|
||||
4. **Deployable**: Ready for production environments
|
||||
5. **Maintainable**: Clear separation of concerns
|
||||
6. **Community-friendly**: Easy for contributors to understand
|
||||
58
docs/SECURITY.md
Normal file
58
docs/SECURITY.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We currently support the following versions with security updates:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.0.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take the security of TechIRCd seriously. If you believe you have found a security vulnerability, please report it to us as described below.
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them via email to: security@techircd.org (or the maintainer's email)
|
||||
|
||||
Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
- Full paths of source file(s) related to the manifestation of the issue
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if possible)
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Response Time
|
||||
|
||||
We will respond to your report within 48 hours and provide regular updates at least every 72 hours.
|
||||
|
||||
## Security Measures
|
||||
|
||||
TechIRCd implements several security measures:
|
||||
|
||||
- Input validation and sanitization
|
||||
- Flood protection and rate limiting
|
||||
- Connection timeout management
|
||||
- Panic recovery mechanisms
|
||||
- Memory usage monitoring
|
||||
- Secure configuration validation
|
||||
|
||||
## Responsible Disclosure
|
||||
|
||||
We follow responsible disclosure practices:
|
||||
|
||||
1. **Report**: Submit vulnerability report privately
|
||||
2. **Acknowledge**: We acknowledge receipt within 48 hours
|
||||
3. **Investigate**: We investigate and develop a fix
|
||||
4. **Coordinate**: We coordinate disclosure timeline with reporter
|
||||
5. **Release**: We release security update and public disclosure
|
||||
218
docs/SERVICES_INTEGRATION.md
Normal file
218
docs/SERVICES_INTEGRATION.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Services Integration Guide
|
||||
|
||||
TechIRCd is designed to work with external IRC services packages like Anope, Atheme, or IRCServices. This document explains how to set up services integration.
|
||||
|
||||
## Supported Services Packages
|
||||
|
||||
### Recommended: Anope Services
|
||||
- **Website**: https://www.anope.org/
|
||||
- **Features**: NickServ, ChanServ, OperServ, MemoServ, BotServ, HostServ
|
||||
- **Database**: MySQL, PostgreSQL, SQLite
|
||||
- **Protocol**: Full IRCd linking support
|
||||
|
||||
### Alternative: Atheme Services
|
||||
- **Website**: https://atheme.github.io/
|
||||
- **Features**: NickServ, ChanServ, OperServ, MemoServ
|
||||
- **Database**: Multiple backend support
|
||||
- **Protocol**: Standard IRC linking
|
||||
|
||||
## TechIRCd Services Integration Features
|
||||
|
||||
### 1. Standard IRC Protocol Support
|
||||
TechIRCd implements the standard IRC protocol that services packages expect:
|
||||
- **Server-to-Server linking** (`linking.json` configuration)
|
||||
- **Standard IRC commands** (NICK, JOIN, PART, PRIVMSG, etc.)
|
||||
- **Operator commands** (KILL, SQUIT, STATS, etc.)
|
||||
- **User and channel modes**
|
||||
- **IRCv3 capabilities** for enhanced features
|
||||
|
||||
### 2. Services Connection Methods
|
||||
|
||||
#### Method A: Services as Linked Server (Recommended)
|
||||
Configure services to connect as a linked server using the linking configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"links": [
|
||||
{
|
||||
"name": "services.yourdomain.com",
|
||||
"host": "127.0.0.1",
|
||||
"port": 6697,
|
||||
"password": "services-link-password",
|
||||
"auto_connect": true,
|
||||
"hub": false,
|
||||
"class": "services"
|
||||
}
|
||||
],
|
||||
"classes": {
|
||||
"services": {
|
||||
"recvq": 16384,
|
||||
"sendq": 16384,
|
||||
"max_links": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Method B: Services as Local Client
|
||||
Services can also connect as regular IRC clients with special privileges.
|
||||
|
||||
### 3. Services User Modes
|
||||
TechIRCd supports standard services user modes:
|
||||
- `+S` - Service mode (identifies services bots)
|
||||
- `+o` - IRC Operator mode
|
||||
- `+B` - Bot mode
|
||||
- `+r` - Registered user mode
|
||||
|
||||
### 4. Channel Modes for Services
|
||||
- `+r` - Registered channel
|
||||
- `+R` - Registered users only
|
||||
- `+M` - Registered/voiced users only speak
|
||||
- `+S` - Services bots only
|
||||
|
||||
## Anope Configuration Example
|
||||
|
||||
### anope.conf snippet:
|
||||
```
|
||||
uplink
|
||||
{
|
||||
host = "127.0.0.1"
|
||||
port = 6667
|
||||
password = "link-password"
|
||||
}
|
||||
|
||||
serverinfo
|
||||
{
|
||||
name = "services.yourdomain.com"
|
||||
description = "Services for TechNet"
|
||||
}
|
||||
|
||||
networkinfo
|
||||
{
|
||||
networkname = "TechNet"
|
||||
ircd = "techircd"
|
||||
}
|
||||
```
|
||||
|
||||
### TechIRCd linking.json for Anope:
|
||||
```json
|
||||
{
|
||||
"linking": {
|
||||
"enable": true,
|
||||
"server_port": 6697,
|
||||
"password": "link-password",
|
||||
"links": [
|
||||
{
|
||||
"name": "services.yourdomain.com",
|
||||
"host": "127.0.0.1",
|
||||
"port": 6697,
|
||||
"password": "link-password",
|
||||
"auto_connect": false,
|
||||
"hub": false,
|
||||
"class": "services"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Configure TechIRCd
|
||||
1. Edit `configs/linking.json` to add services link
|
||||
2. Set up linking passwords
|
||||
3. Configure operator access for services management
|
||||
|
||||
### 2. Install Anope Services
|
||||
```bash
|
||||
# Download and compile Anope
|
||||
wget https://github.com/anope/anope/releases/latest
|
||||
tar -xzf anope-*.tar.gz
|
||||
cd anope-*
|
||||
./configure --prefix=/opt/anope
|
||||
make && make install
|
||||
```
|
||||
|
||||
### 3. Configure Anope
|
||||
1. Edit `/opt/anope/conf/services.conf`
|
||||
2. Set TechIRCd as the uplink server
|
||||
3. Configure database settings
|
||||
4. Set up services nicknames and channels
|
||||
|
||||
### 4. Start Services
|
||||
```bash
|
||||
# Start TechIRCd first
|
||||
./techircd start
|
||||
|
||||
# Then start Anope services
|
||||
/opt/anope/bin/anoperc start
|
||||
```
|
||||
|
||||
## Services Protocol Support
|
||||
|
||||
TechIRCd supports the standard IRC server protocol features needed by services:
|
||||
|
||||
### User Management
|
||||
- `NICK` - Nickname changes
|
||||
- `USER` - User registration
|
||||
- `QUIT` - User disconnection
|
||||
- `KILL` - Force disconnect users
|
||||
|
||||
### Channel Management
|
||||
- `JOIN/PART` - Channel membership
|
||||
- `MODE` - Channel and user modes
|
||||
- `TOPIC` - Channel topics
|
||||
- `KICK/BAN` - Channel moderation
|
||||
|
||||
### Network Management
|
||||
- `SQUIT` - Server disconnection
|
||||
- `SERVER` - Server introduction
|
||||
- `BURST` - Network state synchronization
|
||||
- `STATS` - Server statistics
|
||||
|
||||
### Services-Specific
|
||||
- `SVSNICK` - Services nickname enforcement
|
||||
- `SVSMODE` - Services mode changes
|
||||
- `SVSJOIN` - Services-forced joins
|
||||
- `SVSPART` - Services-forced parts
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Services Won't Connect
|
||||
1. Check linking password in both configs
|
||||
2. Verify port configuration
|
||||
3. Check TechIRCd logs for connection attempts
|
||||
4. Ensure services hostname resolves
|
||||
|
||||
### Services Commands Not Working
|
||||
1. Verify services have operator status
|
||||
2. Check services user modes (+S +o)
|
||||
3. Review channel access levels
|
||||
4. Check services configuration
|
||||
|
||||
### Database Issues
|
||||
Services handle their own database - TechIRCd doesn't need database access.
|
||||
|
||||
## Benefits of External Services
|
||||
|
||||
### For TechIRCd:
|
||||
- ✅ **Simplified codebase** - Focus on IRC protocol
|
||||
- ✅ **Better performance** - No database overhead
|
||||
- ✅ **Higher reliability** - Services crashes don't affect IRC
|
||||
- ✅ **Easier maintenance** - Separate concerns
|
||||
|
||||
### For Users:
|
||||
- ✅ **Mature services** - Anope/Atheme are battle-tested
|
||||
- ✅ **Rich features** - Full services functionality
|
||||
- ✅ **Database choice** - MySQL, PostgreSQL, SQLite
|
||||
- ✅ **Web interfaces** - Many services offer web panels
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **TechIRCd Support**: GitHub Issues
|
||||
- **Anope Support**: https://www.anope.org/
|
||||
- **Atheme Support**: https://atheme.github.io/
|
||||
- **IRC Help**: #anope or #atheme on irc.anope.org
|
||||
208
docs/WHOIS_CONFIGURATION.md
Normal file
208
docs/WHOIS_CONFIGURATION.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# WHOIS Configuration Guide
|
||||
|
||||
TechIRCd provides extremely flexible WHOIS configuration that allows administrators to control exactly what information is visible to different types of users.
|
||||
|
||||
## Overview
|
||||
|
||||
The WHOIS system in TechIRCd uses a three-tier permission system:
|
||||
- **to_everyone**: Information visible to all users
|
||||
- **to_opers**: Information visible only to IRC operators
|
||||
- **to_self**: Information visible when users query themselves
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Basic Information Controls
|
||||
|
||||
#### User Modes (`show_user_modes`)
|
||||
Controls who can see what user modes (+i, +w, +s, etc.) a user has set.
|
||||
|
||||
```json
|
||||
"show_user_modes": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
#### SSL Status (`show_ssl_status`)
|
||||
Shows whether a user is connected via SSL/TLS.
|
||||
|
||||
```json
|
||||
"show_ssl_status": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Idle Time (`show_idle_time`)
|
||||
Shows how long a user has been idle and their signon time.
|
||||
|
||||
```json
|
||||
"show_idle_time": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Signon Time (`show_signon_time`)
|
||||
Shows when a user connected to the server (alternative to idle time).
|
||||
|
||||
```json
|
||||
"show_signon_time": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Real Host (`show_real_host`)
|
||||
Shows the user's real IP address/hostname (bypasses host masking).
|
||||
|
||||
```json
|
||||
"show_real_host": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
### Channel Information (`show_channels`)
|
||||
|
||||
Controls channel visibility with additional granular options:
|
||||
|
||||
```json
|
||||
"show_channels": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true,
|
||||
"hide_secret_channels": true,
|
||||
"hide_private_channels": false,
|
||||
"show_membership_levels": true
|
||||
}
|
||||
```
|
||||
|
||||
- `hide_secret_channels`: Hide channels with mode +s from non-members
|
||||
- `hide_private_channels`: Hide channels with mode +p from non-members
|
||||
- `show_membership_levels`: Show @/+/% prefixes for ops/voice/halfop
|
||||
|
||||
### Operator Information (`show_oper_class`)
|
||||
Shows IRC operator class/type information.
|
||||
|
||||
```json
|
||||
"show_oper_class": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
### Client Information (`show_client_info`)
|
||||
Shows information about the IRC client software being used.
|
||||
|
||||
```json
|
||||
"show_client_info": {
|
||||
"to_everyone": false,
|
||||
"to_opers": true,
|
||||
"to_self": false
|
||||
}
|
||||
```
|
||||
|
||||
### Account Name (`show_account_name`)
|
||||
Shows services account name (for networks with services integration).
|
||||
|
||||
```json
|
||||
"show_account_name": {
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features (Future)
|
||||
|
||||
These features are planned for future implementation:
|
||||
|
||||
- `show_activity_stats`: User activity analytics
|
||||
- `show_github_integration`: GitHub profile integration
|
||||
- `show_geolocation`: Approximate location information
|
||||
- `show_performance_stats`: Connection performance metrics
|
||||
- `show_device_info`: Device and OS information
|
||||
- `show_social_graph`: Mutual channels and connections
|
||||
- `show_security_score`: Account security rating
|
||||
|
||||
## Custom Fields
|
||||
|
||||
TechIRCd supports custom WHOIS fields for maximum flexibility:
|
||||
|
||||
```json
|
||||
"custom_fields": [
|
||||
{
|
||||
"name": "website",
|
||||
"to_everyone": true,
|
||||
"to_opers": true,
|
||||
"to_self": true,
|
||||
"format": "Website: %s",
|
||||
"description": "User's personal website"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Maximum Privacy
|
||||
Only show basic information to everyone, detailed info to opers:
|
||||
|
||||
```json
|
||||
"whois_features": {
|
||||
"show_user_modes": {"to_everyone": false, "to_opers": true, "to_self": true},
|
||||
"show_ssl_status": {"to_everyone": false, "to_opers": true, "to_self": true},
|
||||
"show_idle_time": {"to_everyone": false, "to_opers": true, "to_self": true},
|
||||
"show_real_host": {"to_everyone": false, "to_opers": true, "to_self": true},
|
||||
"show_channels": {
|
||||
"to_everyone": true, "to_opers": true, "to_self": true,
|
||||
"hide_secret_channels": true, "hide_private_channels": true,
|
||||
"show_membership_levels": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Maximum Transparency
|
||||
Show most information to everyone:
|
||||
|
||||
```json
|
||||
"whois_features": {
|
||||
"show_user_modes": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_ssl_status": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_idle_time": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_real_host": {"to_everyone": false, "to_opers": true, "to_self": true},
|
||||
"show_channels": {
|
||||
"to_everyone": true, "to_opers": true, "to_self": true,
|
||||
"hide_secret_channels": false, "hide_private_channels": false,
|
||||
"show_membership_levels": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Development/Testing
|
||||
Show everything to everyone for debugging:
|
||||
|
||||
```json
|
||||
"whois_features": {
|
||||
"show_user_modes": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_ssl_status": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_idle_time": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_real_host": {"to_everyone": true, "to_opers": true, "to_self": true},
|
||||
"show_client_info": {"to_everyone": true, "to_opers": true, "to_self": true}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The WHOIS system respects IRC operator privileges and host masking settings
|
||||
- Secret and private channel hiding works in conjunction with channel membership
|
||||
- All settings are hot-reloadable (restart server after config changes)
|
||||
- The system is designed to be extremely flexible while maintaining IRC protocol compliance
|
||||
|
||||
This configuration system makes TechIRCd one of the most configurable IRC servers available!
|
||||
263
extreme_stress.py
Normal file
263
extreme_stress.py
Normal file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
EXTREME TechIRCd Stress Testing Tool
|
||||
BRUTAL IRC server stress testing designed to break things
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
from typing import List
|
||||
import threading
|
||||
|
||||
class ExtremeIRCClient:
|
||||
def __init__(self, client_id: int):
|
||||
self.client_id = client_id
|
||||
self.nick = f"EXTREME{client_id:05d}"
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.connected = False
|
||||
self.message_count = 0
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.wait_for(
|
||||
asyncio.open_connection('localhost', 6667),
|
||||
timeout=5.0
|
||||
)
|
||||
self.connected = True
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
async def register(self):
|
||||
if not self.connected or self.writer is None:
|
||||
return False
|
||||
try:
|
||||
self.writer.write(f"NICK {self.nick}\r\n".encode())
|
||||
self.writer.write(f"USER {self.nick} 0 * :Extreme Client {self.client_id}\r\n".encode())
|
||||
await self.writer.drain()
|
||||
# Don't wait for response - BRUTAL mode
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
async def spam_messages(self, count: int, delay: float = 0.001):
|
||||
"""Send messages as fast as possible"""
|
||||
if not self.connected or self.writer is None:
|
||||
return
|
||||
|
||||
messages = [
|
||||
f"PRIVMSG #extreme :SPAM MESSAGE {i} FROM {self.nick} - " + "X" * 100
|
||||
for i in range(count)
|
||||
]
|
||||
|
||||
try:
|
||||
for msg in messages:
|
||||
self.writer.write(f"{msg}\r\n".encode())
|
||||
self.message_count += 1
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
await self.writer.drain()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def chaos_commands(self):
|
||||
"""Send random commands rapidly"""
|
||||
commands = [
|
||||
"JOIN #extreme",
|
||||
"PART #extreme",
|
||||
"JOIN #chaos1,#chaos2,#chaos3,#chaos4,#chaos5",
|
||||
"LIST",
|
||||
"WHO #extreme",
|
||||
"VERSION",
|
||||
"TIME",
|
||||
"ADMIN",
|
||||
"INFO",
|
||||
"LUSERS",
|
||||
f"NICK {self.nick}_CHAOS_{random.randint(1000,9999)}",
|
||||
"PING :test",
|
||||
]
|
||||
|
||||
try:
|
||||
if self.writer is not None:
|
||||
for _ in range(50): # 50 rapid commands
|
||||
cmd = random.choice(commands)
|
||||
self.writer.write(f"{cmd}\r\n".encode())
|
||||
await asyncio.sleep(0.01) # 10ms between commands
|
||||
await self.writer.drain()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def disconnect(self):
|
||||
if self.writer:
|
||||
try:
|
||||
self.writer.write(b"QUIT :EXTREME TEST COMPLETE\r\n")
|
||||
await self.writer.drain()
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
except:
|
||||
pass
|
||||
self.connected = False
|
||||
|
||||
class ExtremeStressTester:
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
self.total_connections = 0
|
||||
self.successful_connections = 0
|
||||
self.total_messages = 0
|
||||
self.start_time = None
|
||||
|
||||
async def extreme_connection_bomb(self, count: int):
|
||||
"""Connect as many clients as possible simultaneously"""
|
||||
print(f"🔥 EXTREME: Launching {count} connections simultaneously...")
|
||||
|
||||
tasks = []
|
||||
for i in range(count):
|
||||
client = ExtremeIRCClient(i)
|
||||
self.clients.append(client)
|
||||
tasks.append(self.connect_and_register(client))
|
||||
|
||||
# Launch ALL connections at once - BRUTAL
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
self.successful_connections = sum(1 for r in results if r is True)
|
||||
print(f"💀 Connection bomb result: {self.successful_connections}/{count} successful")
|
||||
|
||||
async def connect_and_register(self, client):
|
||||
try:
|
||||
if await client.connect():
|
||||
await client.register()
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
async def extreme_message_hurricane(self, messages_per_client: int = 200):
|
||||
"""Send massive amounts of messages from all clients"""
|
||||
print(f"🌪️ EXTREME: Message hurricane - {messages_per_client} msgs per client...")
|
||||
|
||||
connected_clients = [c for c in self.clients if c.connected]
|
||||
print(f"💀 Using {len(connected_clients)} connected clients")
|
||||
|
||||
tasks = []
|
||||
for client in connected_clients:
|
||||
tasks.append(client.spam_messages(messages_per_client, delay=0.001)) # 1ms delay
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
total_msgs = sum(c.message_count for c in connected_clients)
|
||||
print(f"💀 Hurricane complete: {total_msgs} total messages sent")
|
||||
|
||||
async def extreme_chaos_mode(self):
|
||||
"""Every client sends random commands rapidly"""
|
||||
print(f"🔥 EXTREME: CHAOS MODE ACTIVATED...")
|
||||
|
||||
connected_clients = [c for c in self.clients if c.connected]
|
||||
tasks = []
|
||||
for client in connected_clients:
|
||||
tasks.append(client.chaos_commands())
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
print(f"💀 Chaos mode complete")
|
||||
|
||||
async def rapid_fire_connections(self, total: int, batch_size: int = 50):
|
||||
"""Connect clients in rapid batches"""
|
||||
print(f"⚡ EXTREME: Rapid fire - {total} clients in batches of {batch_size}")
|
||||
|
||||
for batch_start in range(0, total, batch_size):
|
||||
batch_end = min(batch_start + batch_size, total)
|
||||
batch_clients = []
|
||||
|
||||
# Create batch
|
||||
for i in range(batch_start, batch_end):
|
||||
client = ExtremeIRCClient(i + 10000) # Offset IDs
|
||||
batch_clients.append(client)
|
||||
self.clients.append(client)
|
||||
|
||||
# Connect batch simultaneously
|
||||
tasks = [self.connect_and_register(c) for c in batch_clients]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
successful = sum(1 for r in results if r is True)
|
||||
print(f"💀 Batch {batch_start//batch_size + 1}: {successful}/{len(batch_clients)} connected")
|
||||
|
||||
# Tiny delay between batches
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def cleanup(self):
|
||||
"""Disconnect all clients"""
|
||||
print("🧹 Cleaning up connections...")
|
||||
tasks = []
|
||||
for client in self.clients:
|
||||
if client.connected:
|
||||
tasks.append(client.disconnect())
|
||||
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
print(f"💀 Cleanup complete. Total clients created: {len(self.clients)}")
|
||||
|
||||
async def run_extreme_test():
|
||||
"""Run the most brutal stress test possible"""
|
||||
print("💀💀💀 EXTREME STRESS TEST STARTING 💀💀💀")
|
||||
print("⚠️ WARNING: This test is designed to break things!")
|
||||
print()
|
||||
|
||||
tester = ExtremeStressTester()
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Phase 1: Connection bomb
|
||||
await tester.extreme_connection_bomb(150)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Phase 2: Message hurricane
|
||||
await tester.extreme_message_hurricane(100)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Phase 3: Chaos mode
|
||||
await tester.extreme_chaos_mode()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Phase 4: More connections while under load
|
||||
await tester.rapid_fire_connections(100, 25)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Phase 5: Final message barrage
|
||||
await tester.extreme_message_hurricane(50)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Test interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"\n💥 Test crashed: {e}")
|
||||
finally:
|
||||
# Always cleanup
|
||||
await tester.cleanup()
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
print(f"\n💀 EXTREME STRESS TEST COMPLETE 💀")
|
||||
print(f"Duration: {duration:.2f} seconds")
|
||||
print(f"Total clients: {len(tester.clients)}")
|
||||
print(f"Successful connections: {tester.successful_connections}")
|
||||
total_messages = sum(c.message_count for c in tester.clients)
|
||||
print(f"Total messages sent: {total_messages}")
|
||||
if duration > 0:
|
||||
print(f"Messages per second: {total_messages/duration:.2f}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("💀 EXTREME STRESS TESTER 💀")
|
||||
print("This will attempt to overwhelm the IRC server")
|
||||
print("Make sure TechIRCd is running!")
|
||||
print()
|
||||
|
||||
try:
|
||||
asyncio.run(run_extreme_test())
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Aborted by user")
|
||||
105
extreme_stress.sh
Normal file
105
extreme_stress.sh
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
# EXTREME TechIRCd Stress Test Runner
|
||||
# This script runs progressively more brutal stress tests
|
||||
|
||||
echo "💀💀💀 EXTREME STRESS TEST SUITE 💀💀💀"
|
||||
echo ""
|
||||
|
||||
# Check if TechIRCd is running
|
||||
if ! pgrep -f "techircd" > /dev/null; then
|
||||
echo "🚨 TechIRCd is not running! Starting it..."
|
||||
./techircd start &
|
||||
sleep 3
|
||||
echo "✅ TechIRCd started"
|
||||
fi
|
||||
|
||||
echo "🔥 Available EXTREME stress tests:"
|
||||
echo ""
|
||||
echo "1. extreme_bomb - 150 simultaneous connections + message flood"
|
||||
echo "2. extreme_rapid - Rapid fire connection test (original + extreme scenarios)"
|
||||
echo "3. extreme_nuclear - THE NUCLEAR OPTION (300 clients, everything at max)"
|
||||
echo "4. extreme_python - Custom Python brutality test"
|
||||
echo "5. extreme_all - ALL EXTREME TESTS IN SEQUENCE"
|
||||
echo ""
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <test_name>"
|
||||
echo "Example: $0 extreme_bomb"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEST_TYPE=$1
|
||||
|
||||
case $TEST_TYPE in
|
||||
extreme_bomb)
|
||||
echo "💣 Running EXTREME Mass Connection Bomb..."
|
||||
python3 stress_test.py --scenario "EXTREME: Mass Connection Bomb"
|
||||
;;
|
||||
extreme_rapid)
|
||||
echo "⚡ Running EXTREME Rapid Fire Connections..."
|
||||
python3 stress_test.py --scenario "EXTREME: Rapid Fire Connections"
|
||||
;;
|
||||
extreme_nuclear)
|
||||
echo "☢️ Running THE NUCLEAR OPTION..."
|
||||
echo "⚠️ WARNING: This test uses 300 clients!"
|
||||
python3 stress_test.py --scenario "EXTREME: The Nuclear Option"
|
||||
;;
|
||||
extreme_python)
|
||||
echo "🐍 Running Custom Python Brutality Test..."
|
||||
python3 extreme_stress.py
|
||||
;;
|
||||
extreme_hurricane)
|
||||
echo "🌪️ Running EXTREME Message Hurricane..."
|
||||
python3 stress_test.py --scenario "EXTREME: Message Hurricane"
|
||||
;;
|
||||
extreme_chaos)
|
||||
echo "🔥 Running EXTREME Connection Chaos..."
|
||||
python3 stress_test.py --scenario "EXTREME: Connection Chaos"
|
||||
;;
|
||||
extreme_all)
|
||||
echo "💀 RUNNING ALL EXTREME TESTS IN SEQUENCE..."
|
||||
echo "⚠️ This will be BRUTAL on your server!"
|
||||
read -p "Are you sure? (yes/NO): " confirm
|
||||
if [ "$confirm" = "yes" ]; then
|
||||
echo ""
|
||||
echo "🚀 Starting extreme test sequence..."
|
||||
|
||||
echo "1/6: Mass Connection Bomb"
|
||||
python3 stress_test.py --scenario "EXTREME: Mass Connection Bomb"
|
||||
sleep 5
|
||||
|
||||
echo "2/6: Rapid Fire Connections"
|
||||
python3 stress_test.py --scenario "EXTREME: Rapid Fire Connections"
|
||||
sleep 5
|
||||
|
||||
echo "3/6: Message Hurricane"
|
||||
python3 stress_test.py --scenario "EXTREME: Message Hurricane"
|
||||
sleep 5
|
||||
|
||||
echo "4/6: Connection Chaos"
|
||||
python3 stress_test.py --scenario "EXTREME: Connection Chaos"
|
||||
sleep 5
|
||||
|
||||
echo "5/6: Custom Python Brutality"
|
||||
python3 extreme_stress.py
|
||||
sleep 10
|
||||
|
||||
echo "6/6: THE NUCLEAR OPTION"
|
||||
python3 stress_test.py --scenario "EXTREME: The Nuclear Option"
|
||||
|
||||
echo "💀 ALL EXTREME TESTS COMPLETE 💀"
|
||||
else
|
||||
echo "❌ Extreme test sequence cancelled"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown test type: $TEST_TYPE"
|
||||
echo "Available: extreme_bomb, extreme_rapid, extreme_nuclear, extreme_python, extreme_hurricane, extreme_chaos, extreme_all"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "🏁 Stress test complete!"
|
||||
echo "Check server logs for performance data"
|
||||
99
health.go
Normal file
99
health.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HealthMonitor tracks server health metrics
|
||||
type HealthMonitor struct {
|
||||
server *Server
|
||||
totalClients int64
|
||||
totalMessages int64
|
||||
startTime time.Time
|
||||
ticker *time.Ticker
|
||||
shutdown chan bool
|
||||
}
|
||||
|
||||
func NewHealthMonitor(server *Server) *HealthMonitor {
|
||||
return &HealthMonitor{
|
||||
server: server,
|
||||
startTime: time.Now(),
|
||||
shutdown: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) Start() {
|
||||
h.ticker = time.NewTicker(5 * time.Minute) // Log stats every 5 minutes
|
||||
go h.monitor()
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) Stop() {
|
||||
if h.ticker != nil {
|
||||
h.ticker.Stop()
|
||||
}
|
||||
close(h.shutdown)
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) IncrementClients() {
|
||||
atomic.AddInt64(&h.totalClients, 1)
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) DecrementClients() {
|
||||
atomic.AddInt64(&h.totalClients, -1)
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) IncrementMessages() {
|
||||
atomic.AddInt64(&h.totalMessages, 1)
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) monitor() {
|
||||
for {
|
||||
select {
|
||||
case <-h.ticker.C:
|
||||
h.logHealthStats()
|
||||
case <-h.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HealthMonitor) logHealthStats() {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
h.server.mu.RLock()
|
||||
clientCount := len(h.server.clients)
|
||||
channelCount := len(h.server.channels)
|
||||
h.server.mu.RUnlock()
|
||||
|
||||
totalClients := atomic.LoadInt64(&h.totalClients)
|
||||
totalMessages := atomic.LoadInt64(&h.totalMessages)
|
||||
uptime := time.Since(h.startTime)
|
||||
|
||||
log.Printf("Health Stats - Uptime: %v, Clients: %d, Channels: %d, Total Clients: %d, Total Messages: %d",
|
||||
uptime.Round(time.Second), clientCount, channelCount, totalClients, totalMessages)
|
||||
|
||||
log.Printf("Memory Stats - Alloc: %d KB, Sys: %d KB, NumGC: %d, Goroutines: %d",
|
||||
bToKb(m.Alloc), bToKb(m.Sys), m.NumGC, runtime.NumGoroutine())
|
||||
|
||||
// Alert if memory usage is high
|
||||
if m.Alloc > 100*1024*1024 { // 100MB
|
||||
log.Printf("WARNING: High memory usage detected: %d MB", bToMb(m.Alloc))
|
||||
}
|
||||
|
||||
// Alert if too many goroutines
|
||||
if runtime.NumGoroutine() > 1000 {
|
||||
log.Printf("WARNING: High goroutine count: %d", runtime.NumGoroutine())
|
||||
}
|
||||
}
|
||||
|
||||
func bToKb(b uint64) uint64 {
|
||||
return b / 1024
|
||||
}
|
||||
|
||||
func bToMb(b uint64) uint64 {
|
||||
return b / 1024 / 1024
|
||||
}
|
||||
1963
internal/commands/commands.go
Normal file
1963
internal/commands/commands.go
Normal file
File diff suppressed because it is too large
Load Diff
397
linking.go
Normal file
397
linking.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LinkedServer represents a connection to another IRC server
|
||||
type LinkedServer struct {
|
||||
name string
|
||||
conn net.Conn
|
||||
host string
|
||||
port int
|
||||
password string
|
||||
hub bool
|
||||
description string
|
||||
connected bool
|
||||
lastPing time.Time
|
||||
server *Server
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// LinkConfig represents configuration for server linking
|
||||
type LinkConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
ServerPort int `json:"server_port"`
|
||||
Password string `json:"password"`
|
||||
Hub bool `json:"hub"`
|
||||
AutoConnect bool `json:"auto_connect"`
|
||||
Links []struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Password string `json:"password"`
|
||||
AutoConnect bool `json:"auto_connect"`
|
||||
Hub bool `json:"hub"`
|
||||
Description string `json:"description"`
|
||||
} `json:"links"`
|
||||
}
|
||||
|
||||
// ServerMessage represents a server-to-server message
|
||||
type ServerMessage struct {
|
||||
Prefix string
|
||||
Command string
|
||||
Params []string
|
||||
Source string
|
||||
Target string
|
||||
}
|
||||
|
||||
// NewLinkedServer creates a new linked server connection
|
||||
func NewLinkedServer(name, host string, port int, password string, hub bool, description string, server *Server) *LinkedServer {
|
||||
return &LinkedServer{
|
||||
name: name,
|
||||
host: host,
|
||||
port: port,
|
||||
password: password,
|
||||
hub: hub,
|
||||
description: description,
|
||||
server: server,
|
||||
connected: false,
|
||||
lastPing: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes a connection to the remote server
|
||||
func (ls *LinkedServer) Connect() error {
|
||||
ls.mu.Lock()
|
||||
defer ls.mu.Unlock()
|
||||
|
||||
if ls.connected {
|
||||
return fmt.Errorf("server %s is already connected", ls.name)
|
||||
}
|
||||
|
||||
// Connect to remote server
|
||||
var addr string
|
||||
if strings.Contains(ls.host, ":") {
|
||||
// IPv6 address, enclose in brackets
|
||||
addr = fmt.Sprintf("[%s]:%d", ls.host, ls.port)
|
||||
} else {
|
||||
addr = fmt.Sprintf("%s:%d", ls.host, ls.port)
|
||||
}
|
||||
conn, err := net.DialTimeout("tcp", addr, 30*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to %s: %v", ls.name, err)
|
||||
}
|
||||
|
||||
ls.conn = conn
|
||||
ls.connected = true
|
||||
|
||||
log.Printf("Connected to server %s at %s", ls.name, addr)
|
||||
|
||||
// Start handling the connection
|
||||
go ls.Handle()
|
||||
|
||||
// Send server introduction
|
||||
ls.SendServerAuth()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disconnect closes the connection to the remote server
|
||||
func (ls *LinkedServer) Disconnect() {
|
||||
ls.mu.Lock()
|
||||
defer ls.mu.Unlock()
|
||||
|
||||
if !ls.connected {
|
||||
return
|
||||
}
|
||||
|
||||
if ls.conn != nil {
|
||||
ls.conn.Close()
|
||||
ls.conn = nil
|
||||
}
|
||||
ls.connected = false
|
||||
|
||||
log.Printf("Disconnected from server %s", ls.name)
|
||||
}
|
||||
|
||||
// SendServerAuth sends authentication to the remote server
|
||||
func (ls *LinkedServer) SendServerAuth() {
|
||||
// Send PASS command
|
||||
ls.SendMessage(fmt.Sprintf("PASS %s", ls.password))
|
||||
|
||||
// Send SERVER command
|
||||
ls.SendMessage(fmt.Sprintf("SERVER %s 1 :%s", ls.server.config.Server.Name, ls.server.config.Server.Description))
|
||||
|
||||
log.Printf("Sent authentication to server %s", ls.name)
|
||||
}
|
||||
|
||||
// SendMessage sends a raw message to the linked server
|
||||
func (ls *LinkedServer) SendMessage(message string) error {
|
||||
ls.mu.RLock()
|
||||
defer ls.mu.RUnlock()
|
||||
|
||||
if !ls.connected || ls.conn == nil {
|
||||
return fmt.Errorf("server %s is not connected", ls.name)
|
||||
}
|
||||
|
||||
_, err := fmt.Fprintf(ls.conn, "%s\r\n", message)
|
||||
if err != nil {
|
||||
log.Printf("Error sending message to server %s: %v", ls.name, err)
|
||||
ls.connected = false
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle processes messages from the linked server
|
||||
func (ls *LinkedServer) Handle() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic in server link handler for %s: %v", ls.name, r)
|
||||
}
|
||||
ls.Disconnect()
|
||||
}()
|
||||
|
||||
scanner := net.Conn(ls.conn)
|
||||
buffer := make([]byte, 4096)
|
||||
|
||||
for ls.connected {
|
||||
// Set read deadline
|
||||
ls.conn.SetReadDeadline(time.Now().Add(5 * time.Minute))
|
||||
|
||||
n, err := scanner.Read(buffer)
|
||||
if err != nil {
|
||||
if ls.connected {
|
||||
log.Printf("Read error from server %s: %v", ls.name, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
data := string(buffer[:n])
|
||||
lines := strings.Split(strings.TrimSpace(data), "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ls.handleServerMessage(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleServerMessage processes a single message from the linked server
|
||||
func (ls *LinkedServer) handleServerMessage(line string) {
|
||||
log.Printf("Received from server %s: %s", ls.name, line)
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var command string
|
||||
var params []string
|
||||
|
||||
// Parse message prefix
|
||||
if parts[0][0] == ':' {
|
||||
parts = parts[1:]
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
command = strings.ToUpper(parts[0])
|
||||
params = parts[1:]
|
||||
|
||||
// Handle server-to-server commands
|
||||
switch command {
|
||||
case "PASS":
|
||||
ls.handleServerPass(params)
|
||||
case "SERVER":
|
||||
ls.handleServerIntro(params)
|
||||
case "PING":
|
||||
ls.handleServerPing(params)
|
||||
case "PONG":
|
||||
ls.handleServerPong()
|
||||
case "SQUIT":
|
||||
ls.handleServerQuit(params)
|
||||
case "NICK":
|
||||
ls.handleRemoteNick(params)
|
||||
case "USER":
|
||||
ls.handleRemoteUser(params)
|
||||
case "JOIN":
|
||||
ls.handleRemoteJoin(params)
|
||||
case "PART":
|
||||
ls.handleRemotePart(params)
|
||||
case "QUIT":
|
||||
ls.handleRemoteQuit(params)
|
||||
case "PRIVMSG":
|
||||
ls.handleRemotePrivmsg(params)
|
||||
default:
|
||||
log.Printf("Unknown server command from %s: %s", ls.name, command)
|
||||
}
|
||||
}
|
||||
|
||||
// handleServerPass handles PASS command from remote server
|
||||
func (ls *LinkedServer) handleServerPass(params []string) {
|
||||
if len(params) < 1 {
|
||||
log.Printf("Invalid PASS from server %s", ls.name)
|
||||
ls.Disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
password := params[0]
|
||||
if password != ls.password {
|
||||
log.Printf("Invalid password from server %s", ls.name)
|
||||
ls.Disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Server %s authenticated successfully", ls.name)
|
||||
}
|
||||
|
||||
// handleServerIntro handles SERVER command from remote server
|
||||
func (ls *LinkedServer) handleServerIntro(params []string) {
|
||||
if len(params) < 3 {
|
||||
log.Printf("Invalid SERVER from server %s", ls.name)
|
||||
ls.Disconnect()
|
||||
return
|
||||
}
|
||||
|
||||
serverName := params[0]
|
||||
hopCount := params[1]
|
||||
description := strings.Join(params[2:], " ")
|
||||
|
||||
if description[0] == ':' {
|
||||
description = description[1:]
|
||||
}
|
||||
|
||||
log.Printf("Server introduced: %s (hops: %s) - %s", serverName, hopCount, description)
|
||||
|
||||
// Send our user list and channel list to the remote server
|
||||
ls.sendBurst()
|
||||
}
|
||||
|
||||
// handleServerPing handles PING from remote server
|
||||
func (ls *LinkedServer) handleServerPing(params []string) {
|
||||
if len(params) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
token := params[0]
|
||||
ls.SendMessage(fmt.Sprintf("PONG %s %s", ls.server.config.Server.Name, token))
|
||||
}
|
||||
|
||||
// handleServerPong handles PONG from remote server
|
||||
func (ls *LinkedServer) handleServerPong() {
|
||||
ls.mu.Lock()
|
||||
ls.lastPing = time.Now()
|
||||
ls.mu.Unlock()
|
||||
}
|
||||
|
||||
// handleServerQuit handles SQUIT from remote server
|
||||
func (ls *LinkedServer) handleServerQuit(params []string) {
|
||||
if len(params) >= 1 {
|
||||
reason := strings.Join(params, " ")
|
||||
log.Printf("Server %s quit: %s", ls.name, reason)
|
||||
}
|
||||
ls.Disconnect()
|
||||
}
|
||||
|
||||
// sendBurst sends initial data to newly connected server
|
||||
func (ls *LinkedServer) sendBurst() {
|
||||
// Send all local users
|
||||
for _, client := range ls.server.GetClients() {
|
||||
if client.IsRegistered() {
|
||||
ls.SendMessage(fmt.Sprintf("NICK %s 1 %d %s %s %s %s :%s",
|
||||
client.Nick(),
|
||||
time.Now().Unix(),
|
||||
client.User(),
|
||||
client.Host(),
|
||||
ls.server.config.Server.Name,
|
||||
client.Nick(),
|
||||
client.Realname()))
|
||||
}
|
||||
}
|
||||
|
||||
// Send all channels and their members
|
||||
for _, channel := range ls.server.GetChannels() {
|
||||
// Send channel creation
|
||||
ls.SendMessage(fmt.Sprintf("NJOIN %s :", channel.name))
|
||||
|
||||
// Send channel members
|
||||
var members []string
|
||||
for _, client := range channel.GetClients() {
|
||||
members = append(members, client.Nick())
|
||||
}
|
||||
|
||||
if len(members) > 0 {
|
||||
ls.SendMessage(fmt.Sprintf("NJOIN %s :%s", channel.name, strings.Join(members, " ")))
|
||||
}
|
||||
|
||||
// Send channel topic if exists
|
||||
if channel.topic != "" {
|
||||
ls.SendMessage(fmt.Sprintf("TOPIC %s :%s", channel.name, channel.topic))
|
||||
}
|
||||
}
|
||||
|
||||
// End of burst
|
||||
ls.SendMessage("EOB")
|
||||
log.Printf("Sent burst to server %s", ls.name)
|
||||
}
|
||||
|
||||
// Remote user handling functions
|
||||
func (ls *LinkedServer) handleRemoteNick(params []string) {
|
||||
// Handle remote NICK command
|
||||
log.Printf("Remote NICK from %s: %s", ls.name, strings.Join(params, " "))
|
||||
}
|
||||
|
||||
func (ls *LinkedServer) handleRemoteUser(params []string) {
|
||||
// Handle remote USER command
|
||||
log.Printf("Remote USER from %s: %s", ls.name, strings.Join(params, " "))
|
||||
}
|
||||
|
||||
func (ls *LinkedServer) handleRemoteJoin(params []string) {
|
||||
// Handle remote JOIN command
|
||||
log.Printf("Remote JOIN from %s: %s", ls.name, strings.Join(params, " "))
|
||||
}
|
||||
|
||||
func (ls *LinkedServer) handleRemotePart(params []string) {
|
||||
// Handle remote PART command
|
||||
log.Printf("Remote PART from %s: %s", ls.name, strings.Join(params, " "))
|
||||
}
|
||||
|
||||
func (ls *LinkedServer) handleRemoteQuit(params []string) {
|
||||
// Handle remote QUIT command
|
||||
log.Printf("Remote QUIT from %s: %s", ls.name, strings.Join(params, " "))
|
||||
}
|
||||
|
||||
func (ls *LinkedServer) handleRemotePrivmsg(params []string) {
|
||||
// Handle remote PRIVMSG command
|
||||
log.Printf("Remote PRIVMSG from %s: %s", ls.name, strings.Join(params, " "))
|
||||
}
|
||||
|
||||
// IsConnected returns whether the server is currently connected
|
||||
func (ls *LinkedServer) IsConnected() bool {
|
||||
ls.mu.RLock()
|
||||
defer ls.mu.RUnlock()
|
||||
return ls.connected
|
||||
}
|
||||
|
||||
// Name returns the server name
|
||||
func (ls *LinkedServer) Name() string {
|
||||
return ls.name
|
||||
}
|
||||
|
||||
// Description returns the server description
|
||||
func (ls *LinkedServer) Description() string {
|
||||
return ls.description
|
||||
}
|
||||
334
main.go
Normal file
334
main.go
Normal file
@@ -0,0 +1,334 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DebugMode bool
|
||||
|
||||
const (
|
||||
VERSION = "1.0.0"
|
||||
PIDFILE = "techircd.pid"
|
||||
CONFIGFILE = "config.json"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command line arguments
|
||||
if len(os.Args) < 2 {
|
||||
showUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command := strings.ToLower(os.Args[1])
|
||||
|
||||
// Parse flags for the remaining arguments
|
||||
flagSet := flag.NewFlagSet("techircd", flag.ExitOnError)
|
||||
configFile := flagSet.String("config", CONFIGFILE, "Path to configuration file")
|
||||
daemon := flagSet.Bool("daemon", false, "Run as daemon (background)")
|
||||
verbose := flagSet.Bool("verbose", false, "Enable verbose logging")
|
||||
debug := flagSet.Bool("debug", false, "Enable extremely detailed debug logging (shows all IRC messages)")
|
||||
port := flagSet.Int("port", 0, "Override port from config")
|
||||
|
||||
// Parse remaining arguments
|
||||
flagSet.Parse(os.Args[2:])
|
||||
|
||||
switch command {
|
||||
case "start":
|
||||
startServer(*configFile, *daemon, *verbose, *debug, *port)
|
||||
case "stop":
|
||||
stopServer()
|
||||
case "restart":
|
||||
stopServer()
|
||||
time.Sleep(2 * time.Second)
|
||||
startServer(*configFile, *daemon, *verbose, *debug, *port)
|
||||
case "status":
|
||||
showStatus()
|
||||
case "reload":
|
||||
reloadConfig()
|
||||
case "version", "-v", "--version":
|
||||
fmt.Printf("TechIRCd version %s\n", VERSION)
|
||||
case "help", "-h", "--help":
|
||||
showUsage()
|
||||
default:
|
||||
fmt.Printf("Unknown command: %s\n", command)
|
||||
showUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func showUsage() {
|
||||
fmt.Printf(`TechIRCd %s - Modern IRC Server
|
||||
|
||||
Usage: %s <command> [options]
|
||||
|
||||
Commands:
|
||||
start Start the IRC server
|
||||
stop Stop the IRC server
|
||||
restart Restart the IRC server
|
||||
status Show server status
|
||||
reload Reload configuration
|
||||
version Show version information
|
||||
help Show this help message
|
||||
|
||||
Options:
|
||||
-config <file> Path to configuration file (default: config.json)
|
||||
-daemon Run as daemon (background process)
|
||||
-verbose Enable verbose logging
|
||||
-debug Enable extremely detailed debug logging (shows all IRC messages)
|
||||
-port <port> Override port from configuration
|
||||
|
||||
Examples:
|
||||
%s start # Start with default config
|
||||
%s start -config custom.json # Start with custom config
|
||||
%s start -daemon # Start as background daemon
|
||||
%s stop # Stop the server
|
||||
%s status # Check if server is running
|
||||
|
||||
`, VERSION, os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0])
|
||||
}
|
||||
|
||||
func startServer(configFile string, daemon, verbose, debug bool, port int) {
|
||||
// Check if server is already running
|
||||
if isRunning() {
|
||||
fmt.Println("TechIRCd is already running")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load config from %s, using defaults: %v", configFile, err)
|
||||
config = DefaultConfig()
|
||||
if err := SaveConfig(config, configFile); err != nil {
|
||||
log.Printf("Failed to save default config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Override port if specified
|
||||
if port > 0 {
|
||||
config.Server.Listen.Port = port
|
||||
fmt.Printf("Port overridden to %d\n", port)
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if err := config.Validate(); err != nil {
|
||||
log.Fatalf("Configuration validation failed: %v", err)
|
||||
}
|
||||
config.SanitizeConfig()
|
||||
|
||||
if verbose {
|
||||
log.Println("Configuration validated successfully")
|
||||
}
|
||||
|
||||
// Set global debug mode
|
||||
DebugMode = debug
|
||||
if debug {
|
||||
log.Println("Debug mode enabled - will log all IRC messages")
|
||||
}
|
||||
|
||||
// Start as daemon if requested
|
||||
if daemon {
|
||||
startDaemon(configFile, verbose, debug, port)
|
||||
return
|
||||
}
|
||||
|
||||
// Write PID file
|
||||
if err := writePidFile(); err != nil {
|
||||
log.Fatalf("Failed to write PID file: %v", err)
|
||||
}
|
||||
defer removePidFile()
|
||||
|
||||
// Create server
|
||||
server := NewServer(config)
|
||||
|
||||
// Setup signal handling for graceful shutdown with forced shutdown capability
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||
|
||||
shutdownInProgress := false
|
||||
go func() {
|
||||
for sig := range c {
|
||||
switch sig {
|
||||
case syscall.SIGHUP:
|
||||
log.Println("Received SIGHUP, reloading configuration...")
|
||||
// Reload config here if needed
|
||||
case os.Interrupt, syscall.SIGTERM:
|
||||
if shutdownInProgress {
|
||||
log.Println("Received second interrupt signal, forcing immediate shutdown...")
|
||||
removePidFile()
|
||||
os.Exit(1) // Force exit
|
||||
}
|
||||
|
||||
shutdownInProgress = true
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
// Start shutdown with timeout
|
||||
shutdownComplete := make(chan bool, 1)
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic during shutdown: %v", r)
|
||||
}
|
||||
shutdownComplete <- true
|
||||
}()
|
||||
server.Shutdown()
|
||||
}()
|
||||
|
||||
// Wait for graceful shutdown or force after timeout
|
||||
select {
|
||||
case <-shutdownComplete:
|
||||
log.Println("Graceful shutdown completed")
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Println("Shutdown timeout reached, forcing exit...")
|
||||
}
|
||||
|
||||
removePidFile()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the server
|
||||
fmt.Printf("Starting TechIRCd %s on %s:%d\n", VERSION, config.Server.Listen.Host, config.Server.Listen.Port)
|
||||
if config.Server.Listen.EnableSSL {
|
||||
fmt.Printf("SSL enabled on port %d\n", config.Server.Listen.SSLPort)
|
||||
}
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startDaemon(configFile string, verbose, debug bool, port int) {
|
||||
// Build command arguments
|
||||
args := []string{os.Args[0], "start", "-config", configFile}
|
||||
if verbose {
|
||||
args = append(args, "-verbose")
|
||||
}
|
||||
if debug {
|
||||
args = append(args, "-debug")
|
||||
}
|
||||
if port > 0 {
|
||||
args = append(args, "-port", strconv.Itoa(port))
|
||||
}
|
||||
|
||||
// Start as background process
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.Stdin = nil
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("Failed to start daemon: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("TechIRCd started as daemon (PID: %d)\n", cmd.Process.Pid)
|
||||
}
|
||||
|
||||
func stopServer() {
|
||||
pid, err := readPidFile()
|
||||
if err != nil {
|
||||
fmt.Println("TechIRCd is not running")
|
||||
return
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
fmt.Println("TechIRCd is not running")
|
||||
removePidFile()
|
||||
return
|
||||
}
|
||||
|
||||
// Send SIGTERM for graceful shutdown
|
||||
if err := process.Signal(syscall.SIGTERM); err != nil {
|
||||
fmt.Println("TechIRCd is not running")
|
||||
removePidFile()
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for process to stop
|
||||
for i := 0; i < 10; i++ {
|
||||
if !isRunning() {
|
||||
fmt.Println("TechIRCd stopped")
|
||||
return
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Force kill if still running
|
||||
process.Signal(syscall.SIGKILL)
|
||||
fmt.Println("TechIRCd force stopped")
|
||||
removePidFile()
|
||||
}
|
||||
|
||||
func showStatus() {
|
||||
if isRunning() {
|
||||
pid, _ := readPidFile()
|
||||
fmt.Printf("TechIRCd is running (PID: %d)\n", pid)
|
||||
} else {
|
||||
fmt.Println("TechIRCd is not running")
|
||||
}
|
||||
}
|
||||
|
||||
func reloadConfig() {
|
||||
pid, err := readPidFile()
|
||||
if err != nil {
|
||||
fmt.Println("TechIRCd is not running")
|
||||
return
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
fmt.Println("TechIRCd is not running")
|
||||
return
|
||||
}
|
||||
|
||||
if err := process.Signal(syscall.SIGHUP); err != nil {
|
||||
fmt.Println("Failed to reload configuration")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Configuration reload signal sent")
|
||||
}
|
||||
|
||||
func isRunning() bool {
|
||||
pid, err := readPidFile()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Send signal 0 to check if process exists
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func writePidFile() error {
|
||||
pid := os.Getpid()
|
||||
return os.WriteFile(PIDFILE, []byte(strconv.Itoa(pid)), 0644)
|
||||
}
|
||||
|
||||
func readPidFile() (int, error) {
|
||||
data, err := os.ReadFile(PIDFILE)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
func removePidFile() {
|
||||
os.Remove(PIDFILE)
|
||||
}
|
||||
138
monitoring_analytics.go
Normal file
138
monitoring_analytics.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Advanced monitoring and analytics
|
||||
type MonitoringConfig struct {
|
||||
Prometheus struct {
|
||||
Enable bool `json:"enable"`
|
||||
Port int `json:"port"`
|
||||
Path string `json:"path"`
|
||||
} `json:"prometheus"`
|
||||
|
||||
Grafana struct {
|
||||
Enable bool `json:"enable"`
|
||||
Dashboard string `json:"dashboard_url"`
|
||||
} `json:"grafana"`
|
||||
|
||||
Logging struct {
|
||||
Level string `json:"level"`
|
||||
Format string `json:"format"` // json, text
|
||||
Output string `json:"output"` // file, stdout, syslog
|
||||
Structured bool `json:"structured"`
|
||||
} `json:"logging"`
|
||||
}
|
||||
|
||||
// Metrics collection
|
||||
type ServerMetrics struct {
|
||||
// Connection metrics
|
||||
TotalConnections int64 `metric:"total_connections"`
|
||||
ActiveConnections int `metric:"active_connections"`
|
||||
ConnectionsPerSecond float64 `metric:"connections_per_second"`
|
||||
FailedConnections int64 `metric:"failed_connections"`
|
||||
|
||||
// Message metrics
|
||||
MessagesPerSecond float64 `metric:"messages_per_second"`
|
||||
TotalMessages int64 `metric:"total_messages"`
|
||||
PrivateMessages int64 `metric:"private_messages"`
|
||||
ChannelMessages int64 `metric:"channel_messages"`
|
||||
|
||||
// Channel metrics
|
||||
TotalChannels int `metric:"total_channels"`
|
||||
AverageChannelSize float64 `metric:"average_channel_size"`
|
||||
LargestChannel int `metric:"largest_channel"`
|
||||
|
||||
// Performance metrics
|
||||
CPUUsage float64 `metric:"cpu_usage"`
|
||||
MemoryUsage int64 `metric:"memory_usage"`
|
||||
GoroutineCount int `metric:"goroutine_count"`
|
||||
ResponseTime time.Duration `metric:"response_time"`
|
||||
|
||||
// Network metrics
|
||||
BytesSent int64 `metric:"bytes_sent"`
|
||||
BytesReceived int64 `metric:"bytes_received"`
|
||||
NetworkLatency time.Duration `metric:"network_latency"`
|
||||
}
|
||||
|
||||
// Real-time analytics
|
||||
type AnalyticsEngine struct {
|
||||
UserActivity map[string]*UserActivityMetrics
|
||||
ChannelAnalytics map[string]*ChannelAnalytics
|
||||
NetworkHealth *NetworkHealthMetrics
|
||||
}
|
||||
|
||||
type UserActivityMetrics struct {
|
||||
MessagesPerHour []int `json:"messages_per_hour"`
|
||||
ChannelsJoined []string `json:"channels_joined"`
|
||||
CommandsUsed map[string]int `json:"commands_used"`
|
||||
ConnectionTime time.Duration `json:"connection_time"`
|
||||
LastActivity time.Time `json:"last_activity"`
|
||||
}
|
||||
|
||||
type ChannelAnalytics struct {
|
||||
MessageCount int64 `json:"message_count"`
|
||||
UniqueUsers int `json:"unique_users"`
|
||||
AverageUsers float64 `json:"average_users"`
|
||||
PeakUsers int `json:"peak_users"`
|
||||
MostActiveUsers []string `json:"most_active_users"`
|
||||
TopicChanges int `json:"topic_changes"`
|
||||
}
|
||||
|
||||
type NetworkHealthMetrics struct {
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
ServerLoad float64 `json:"server_load"`
|
||||
ErrorRate float64 `json:"error_rate"`
|
||||
ResponseTime time.Duration `json:"response_time"`
|
||||
LinkedServerCount int `json:"linked_servers"`
|
||||
NetworkSplit bool `json:"network_split"`
|
||||
}
|
||||
|
||||
// Alert system
|
||||
type AlertManager struct {
|
||||
Rules []AlertRule `json:"rules"`
|
||||
Channels []AlertChannel `json:"channels"`
|
||||
}
|
||||
|
||||
type AlertRule struct {
|
||||
Name string `json:"name"`
|
||||
Condition string `json:"condition"` // "cpu_usage > 80"
|
||||
Threshold float64 `json:"threshold"`
|
||||
Duration int `json:"duration"` // seconds
|
||||
Severity string `json:"severity"` // info, warning, critical
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
||||
type AlertChannel struct {
|
||||
Type string `json:"type"` // email, slack, discord, webhook
|
||||
Config map[string]string `json:"config"`
|
||||
}
|
||||
|
||||
// Performance profiling
|
||||
func (s *Server) StartProfiling() {
|
||||
// Enable CPU and memory profiling
|
||||
}
|
||||
|
||||
func (s *Server) GeneratePerformanceReport() *PerformanceReport {
|
||||
// Generate detailed performance report
|
||||
return &PerformanceReport{}
|
||||
}
|
||||
|
||||
type PerformanceReport struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CPUProfile []CPUSample `json:"cpu_profile"`
|
||||
MemoryProfile []MemorySample `json:"memory_profile"`
|
||||
Bottlenecks []string `json:"bottlenecks"`
|
||||
Recommendations []string `json:"recommendations"`
|
||||
}
|
||||
|
||||
type CPUSample struct {
|
||||
Function string `json:"function"`
|
||||
Usage float64 `json:"usage"`
|
||||
}
|
||||
|
||||
type MemorySample struct {
|
||||
Object string `json:"object"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
339
oper_config.go
Normal file
339
oper_config.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// OperClass defines an operator class with specific permissions
|
||||
type OperClass struct {
|
||||
Name string `json:"name"`
|
||||
Rank int `json:"rank"` // Higher number = higher rank
|
||||
Description string `json:"description"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Inherits string `json:"inherits"` // Inherit permissions from another class
|
||||
Color string `json:"color"` // Color for display purposes
|
||||
Symbol string `json:"symbol"` // Symbol to display (*, @, &, etc.)
|
||||
}
|
||||
|
||||
// Oper defines an individual operator
|
||||
type Oper struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"`
|
||||
Class string `json:"class"`
|
||||
Flags []string `json:"flags"` // Additional per-user flags
|
||||
MaxClients int `json:"max_clients"` // Max clients this oper can handle
|
||||
Expires string `json:"expires"` // Expiration date (optional)
|
||||
Contact string `json:"contact"` // Contact information
|
||||
LastSeen string `json:"last_seen"` // Last time this oper was online
|
||||
}
|
||||
|
||||
// RankNames defines custom names for rank levels
|
||||
type RankNames struct {
|
||||
Rank1 string `json:"rank_1"` // Default: Helper
|
||||
Rank2 string `json:"rank_2"` // Default: Moderator
|
||||
Rank3 string `json:"rank_3"` // Default: Operator
|
||||
Rank4 string `json:"rank_4"` // Default: Administrator
|
||||
Rank5 string `json:"rank_5"` // Default: Owner
|
||||
// Support for custom ranks beyond 5
|
||||
CustomRanks map[string]int `json:"custom_ranks"` // "CustomName": 6
|
||||
}
|
||||
|
||||
// OperConfig holds the complete operator configuration
|
||||
type OperConfig struct {
|
||||
Classes []OperClass `json:"classes"`
|
||||
Opers []Oper `json:"opers"`
|
||||
RankNames RankNames `json:"rank_names"`
|
||||
Settings struct {
|
||||
RequireSSL bool `json:"require_ssl"`
|
||||
MaxFailedAttempts int `json:"max_failed_attempts"`
|
||||
LockoutDuration int `json:"lockout_duration_minutes"`
|
||||
AllowedCommands []string `json:"allowed_commands"`
|
||||
LogOperActions bool `json:"log_oper_actions"`
|
||||
NotifyOnOperUp bool `json:"notify_on_oper_up"`
|
||||
AutoExpireInactive int `json:"auto_expire_inactive_days"`
|
||||
RequireTwoFactor bool `json:"require_two_factor"`
|
||||
} `json:"settings"`
|
||||
}
|
||||
|
||||
// LoadOperConfig loads operator configuration from file
|
||||
func LoadOperConfig(filename string) (*OperConfig, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read oper config file: %v", err)
|
||||
}
|
||||
|
||||
var config OperConfig
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse oper config file: %v", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SaveOperConfig saves operator configuration to file
|
||||
func SaveOperConfig(config *OperConfig, filename string) error {
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal oper config: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write oper config file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOperClass returns the operator class by name
|
||||
func (oc *OperConfig) GetOperClass(className string) *OperClass {
|
||||
for i := range oc.Classes {
|
||||
if oc.Classes[i].Name == className {
|
||||
return &oc.Classes[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOper returns the operator by name
|
||||
func (oc *OperConfig) GetOper(operName string) *Oper {
|
||||
for i := range oc.Opers {
|
||||
if oc.Opers[i].Name == operName {
|
||||
return &oc.Opers[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOperPermissions returns all permissions for an operator (including inherited)
|
||||
func (oc *OperConfig) GetOperPermissions(operName string) []string {
|
||||
oper := oc.GetOper(operName)
|
||||
if oper == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
class := oc.GetOperClass(oper.Class)
|
||||
if class == nil {
|
||||
return oper.Flags
|
||||
}
|
||||
|
||||
permissions := make(map[string]bool)
|
||||
|
||||
// Add class permissions
|
||||
for _, perm := range class.Permissions {
|
||||
permissions[perm] = true
|
||||
}
|
||||
|
||||
// Add inherited permissions
|
||||
if class.Inherits != "" {
|
||||
inherited := oc.GetOperClass(class.Inherits)
|
||||
if inherited != nil {
|
||||
for _, perm := range inherited.Permissions {
|
||||
permissions[perm] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add individual flags
|
||||
for _, flag := range oper.Flags {
|
||||
permissions[flag] = true
|
||||
}
|
||||
|
||||
// Convert back to slice
|
||||
result := make([]string, 0, len(permissions))
|
||||
for perm := range permissions {
|
||||
result = append(result, perm)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetRankName returns the custom name for a rank level
|
||||
func (oc *OperConfig) GetRankName(rank int) string {
|
||||
switch rank {
|
||||
case 1:
|
||||
if oc.RankNames.Rank1 != "" {
|
||||
return oc.RankNames.Rank1
|
||||
}
|
||||
return "Helper"
|
||||
case 2:
|
||||
if oc.RankNames.Rank2 != "" {
|
||||
return oc.RankNames.Rank2
|
||||
}
|
||||
return "Moderator"
|
||||
case 3:
|
||||
if oc.RankNames.Rank3 != "" {
|
||||
return oc.RankNames.Rank3
|
||||
}
|
||||
return "Operator"
|
||||
case 4:
|
||||
if oc.RankNames.Rank4 != "" {
|
||||
return oc.RankNames.Rank4
|
||||
}
|
||||
return "Administrator"
|
||||
case 5:
|
||||
if oc.RankNames.Rank5 != "" {
|
||||
return oc.RankNames.Rank5
|
||||
}
|
||||
return "Owner"
|
||||
default:
|
||||
// Check custom ranks
|
||||
for name, rankNum := range oc.RankNames.CustomRanks {
|
||||
if rankNum == rank {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Rank %d", rank)
|
||||
}
|
||||
}
|
||||
|
||||
// GetOperRankName returns the rank name for an operator
|
||||
func (oc *OperConfig) GetOperRankName(operName string) string {
|
||||
oper := oc.GetOper(operName)
|
||||
if oper == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
class := oc.GetOperClass(oper.Class)
|
||||
if class == nil {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
return oc.GetRankName(class.Rank)
|
||||
}
|
||||
|
||||
// SetCustomRankName allows adding custom rank names at runtime
|
||||
func (oc *OperConfig) SetCustomRankName(name string, rank int) {
|
||||
if oc.RankNames.CustomRanks == nil {
|
||||
oc.RankNames.CustomRanks = make(map[string]int)
|
||||
}
|
||||
oc.RankNames.CustomRanks[name] = rank
|
||||
}
|
||||
|
||||
// HasPermission checks if an operator has a specific permission
|
||||
func (oc *OperConfig) HasPermission(operName, permission string) bool {
|
||||
permissions := oc.GetOperPermissions(operName)
|
||||
for _, perm := range permissions {
|
||||
if perm == permission || perm == "*" { // * grants all permissions
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetOperRank returns the rank of an operator
|
||||
func (oc *OperConfig) GetOperRank(operName string) int {
|
||||
oper := oc.GetOper(operName)
|
||||
if oper == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
class := oc.GetOperClass(oper.Class)
|
||||
if class == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return class.Rank
|
||||
}
|
||||
|
||||
// CanOperateOn checks if oper1 can perform actions on oper2 (based on rank)
|
||||
func (oc *OperConfig) CanOperateOn(oper1Name, oper2Name string) bool {
|
||||
rank1 := oc.GetOperRank(oper1Name)
|
||||
rank2 := oc.GetOperRank(oper2Name)
|
||||
|
||||
// Higher rank can operate on lower rank
|
||||
// Same rank cannot operate on each other (unless they have override permission)
|
||||
return rank1 > rank2 || oc.HasPermission(oper1Name, "override_rank")
|
||||
}
|
||||
|
||||
// DefaultOperConfig returns a default operator configuration
|
||||
func DefaultOperConfig() *OperConfig {
|
||||
return &OperConfig{
|
||||
Classes: []OperClass{
|
||||
{
|
||||
Name: "helper",
|
||||
Rank: 1,
|
||||
Description: "Helper - Basic moderation commands",
|
||||
Permissions: []string{"kick", "topic", "mode_channel"},
|
||||
Color: "green",
|
||||
Symbol: "%",
|
||||
},
|
||||
{
|
||||
Name: "moderator",
|
||||
Rank: 2,
|
||||
Description: "Moderator - Channel and user management",
|
||||
Permissions: []string{"ban", "unban", "kick", "mute", "topic", "mode_channel", "mode_user", "who_override"},
|
||||
Inherits: "helper",
|
||||
Color: "blue",
|
||||
Symbol: "@",
|
||||
},
|
||||
{
|
||||
Name: "operator",
|
||||
Rank: 3,
|
||||
Description: "Operator - Server management commands",
|
||||
Permissions: []string{"kill", "gline", "rehash", "connect", "squit", "wallops", "operwall"},
|
||||
Inherits: "moderator",
|
||||
Color: "red",
|
||||
Symbol: "*",
|
||||
},
|
||||
{
|
||||
Name: "admin",
|
||||
Rank: 4,
|
||||
Description: "Administrator - Full server control",
|
||||
Permissions: []string{"*"}, // All permissions
|
||||
Color: "purple",
|
||||
Symbol: "&",
|
||||
},
|
||||
{
|
||||
Name: "owner",
|
||||
Rank: 5,
|
||||
Description: "Server Owner - Ultimate authority",
|
||||
Permissions: []string{"*", "override_rank", "shutdown", "restart"},
|
||||
Color: "gold",
|
||||
Symbol: "~",
|
||||
},
|
||||
},
|
||||
Opers: []Oper{
|
||||
{
|
||||
Name: "admin",
|
||||
Password: "changeme",
|
||||
Host: "*@localhost",
|
||||
Class: "admin",
|
||||
MaxClients: 1000,
|
||||
Contact: "admin@example.com",
|
||||
},
|
||||
},
|
||||
RankNames: RankNames{
|
||||
Rank1: "Helper", // Customizable: "Support Staff", "Junior Mod", etc.
|
||||
Rank2: "Moderator", // Customizable: "Channel Mod", "Guard", etc.
|
||||
Rank3: "Operator", // Customizable: "IRC Operator", "Senior Staff", etc.
|
||||
Rank4: "Administrator", // Customizable: "Admin", "Server Admin", etc.
|
||||
Rank5: "Owner", // Customizable: "Root", "Founder", etc.
|
||||
CustomRanks: map[string]int{
|
||||
// Example: "Super Admin": 6,
|
||||
// Example: "Network Founder": 10,
|
||||
},
|
||||
},
|
||||
Settings: struct {
|
||||
RequireSSL bool `json:"require_ssl"`
|
||||
MaxFailedAttempts int `json:"max_failed_attempts"`
|
||||
LockoutDuration int `json:"lockout_duration_minutes"`
|
||||
AllowedCommands []string `json:"allowed_commands"`
|
||||
LogOperActions bool `json:"log_oper_actions"`
|
||||
NotifyOnOperUp bool `json:"notify_on_oper_up"`
|
||||
AutoExpireInactive int `json:"auto_expire_inactive_days"`
|
||||
RequireTwoFactor bool `json:"require_two_factor"`
|
||||
}{
|
||||
RequireSSL: false,
|
||||
MaxFailedAttempts: 3,
|
||||
LockoutDuration: 30,
|
||||
AllowedCommands: []string{"OPER", "KILL", "GLINE", "REHASH", "WALLOPS"},
|
||||
LogOperActions: true,
|
||||
NotifyOnOperUp: true,
|
||||
AutoExpireInactive: 365,
|
||||
RequireTwoFactor: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
192
rate_limiter.go
Normal file
192
rate_limiter.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AdvancedRateLimiter implements token bucket rate limiting for client connections
|
||||
type AdvancedRateLimiter struct {
|
||||
maxTokens int
|
||||
refillRate time.Duration
|
||||
tokens int
|
||||
lastRefill time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// ClientRateLimiter manages per-client rate limiting
|
||||
type ClientRateLimiter struct {
|
||||
limiters map[string]*AdvancedRateLimiter
|
||||
mu sync.RWMutex
|
||||
cleanup chan string
|
||||
}
|
||||
|
||||
// NewAdvancedRateLimiter creates a new rate limiter
|
||||
func NewAdvancedRateLimiter(maxTokens int, refillRate time.Duration) *AdvancedRateLimiter {
|
||||
return &AdvancedRateLimiter{
|
||||
maxTokens: maxTokens,
|
||||
refillRate: refillRate,
|
||||
tokens: maxTokens,
|
||||
lastRefill: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Allow checks if an action is allowed (consumes a token)
|
||||
func (rl *AdvancedRateLimiter) Allow() bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
rl.refill()
|
||||
|
||||
if rl.tokens > 0 {
|
||||
rl.tokens--
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// refill adds tokens based on elapsed time
|
||||
func (rl *AdvancedRateLimiter) refill() {
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(rl.lastRefill)
|
||||
|
||||
tokensToAdd := int(elapsed / rl.refillRate)
|
||||
if tokensToAdd > 0 {
|
||||
rl.tokens += tokensToAdd
|
||||
if rl.tokens > rl.maxTokens {
|
||||
rl.tokens = rl.maxTokens
|
||||
}
|
||||
rl.lastRefill = now
|
||||
}
|
||||
}
|
||||
|
||||
// GetTokens returns current token count (for monitoring)
|
||||
func (rl *AdvancedRateLimiter) GetTokens() int {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
rl.refill()
|
||||
return rl.tokens
|
||||
}
|
||||
|
||||
// NewClientRateLimiter creates a new client rate limiter
|
||||
func NewClientRateLimiter() *ClientRateLimiter {
|
||||
crl := &ClientRateLimiter{
|
||||
limiters: make(map[string]*AdvancedRateLimiter),
|
||||
cleanup: make(chan string, 1000),
|
||||
}
|
||||
|
||||
// Start cleanup routine
|
||||
go crl.cleanupRoutine()
|
||||
|
||||
return crl
|
||||
}
|
||||
|
||||
// Allow checks if a client action is allowed
|
||||
func (crl *ClientRateLimiter) Allow(clientID string, maxTokens int, refillRate time.Duration) bool {
|
||||
crl.mu.RLock()
|
||||
limiter, exists := crl.limiters[clientID]
|
||||
crl.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
crl.mu.Lock()
|
||||
// Double-check after acquiring write lock
|
||||
limiter, exists = crl.limiters[clientID]
|
||||
if !exists {
|
||||
limiter = NewAdvancedRateLimiter(maxTokens, refillRate)
|
||||
crl.limiters[clientID] = limiter
|
||||
}
|
||||
crl.mu.Unlock()
|
||||
}
|
||||
|
||||
return limiter.Allow()
|
||||
}
|
||||
|
||||
// RemoveClient removes a client's rate limiter
|
||||
func (crl *ClientRateLimiter) RemoveClient(clientID string) {
|
||||
select {
|
||||
case crl.cleanup <- clientID:
|
||||
// Queued for cleanup
|
||||
default:
|
||||
// Cleanup queue full, clean directly
|
||||
crl.mu.Lock()
|
||||
delete(crl.limiters, clientID)
|
||||
crl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupRoutine processes client cleanup requests
|
||||
func (crl *ClientRateLimiter) cleanupRoutine() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case clientID := <-crl.cleanup:
|
||||
crl.mu.Lock()
|
||||
delete(crl.limiters, clientID)
|
||||
crl.mu.Unlock()
|
||||
|
||||
case <-ticker.C:
|
||||
// Periodic cleanup of old rate limiters
|
||||
crl.mu.Lock()
|
||||
for clientID, limiter := range crl.limiters {
|
||||
// Remove limiters that haven't been used recently
|
||||
if time.Since(limiter.lastRefill) > 10*time.Minute {
|
||||
delete(crl.limiters, clientID)
|
||||
}
|
||||
}
|
||||
crl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns rate limiter statistics
|
||||
func (crl *ClientRateLimiter) GetStats() map[string]interface{} {
|
||||
crl.mu.RLock()
|
||||
defer crl.mu.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats["active_limiters"] = len(crl.limiters)
|
||||
|
||||
totalTokens := 0
|
||||
for _, limiter := range crl.limiters {
|
||||
totalTokens += limiter.GetTokens()
|
||||
}
|
||||
stats["total_tokens"] = totalTokens
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// GlobalRateLimiter implements server-wide rate limiting
|
||||
type GlobalRateLimiter struct {
|
||||
connectionLimiter *AdvancedRateLimiter
|
||||
messageLimiter *AdvancedRateLimiter
|
||||
}
|
||||
|
||||
// NewGlobalRateLimiter creates a new global rate limiter
|
||||
func NewGlobalRateLimiter(connPerSecond, msgPerSecond int) *GlobalRateLimiter {
|
||||
return &GlobalRateLimiter{
|
||||
connectionLimiter: NewAdvancedRateLimiter(connPerSecond*10, time.Second/time.Duration(connPerSecond)),
|
||||
messageLimiter: NewAdvancedRateLimiter(msgPerSecond*10, time.Second/time.Duration(msgPerSecond)),
|
||||
}
|
||||
}
|
||||
|
||||
// AllowConnection checks if a new connection is allowed
|
||||
func (grl *GlobalRateLimiter) AllowConnection() bool {
|
||||
return grl.connectionLimiter.Allow()
|
||||
}
|
||||
|
||||
// AllowMessage checks if a message is allowed
|
||||
func (grl *GlobalRateLimiter) AllowMessage() bool {
|
||||
return grl.messageLimiter.Allow()
|
||||
}
|
||||
|
||||
// GetConnectionTokens returns available connection tokens
|
||||
func (grl *GlobalRateLimiter) GetConnectionTokens() int {
|
||||
return grl.connectionLimiter.GetTokens()
|
||||
}
|
||||
|
||||
// GetMessageTokens returns available message tokens
|
||||
func (grl *GlobalRateLimiter) GetMessageTokens() int {
|
||||
return grl.messageLimiter.GetTokens()
|
||||
}
|
||||
60
scripts/demo_user_modes.sh
Executable file
60
scripts/demo_user_modes.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
# God Mode and Stealth Mode User Mode Demo
|
||||
# This script demonstrates the new user mode implementation
|
||||
|
||||
echo "=== God Mode and Stealth Mode User Mode Demo ==="
|
||||
echo ""
|
||||
echo "God Mode and Stealth Mode are now implemented as proper IRC user modes:"
|
||||
echo ""
|
||||
|
||||
echo "🔱 GOD MODE (+G):"
|
||||
echo " /mode mynick +G # Enable God Mode - Ultimate channel override powers"
|
||||
echo " /mode mynick -G # Disable God Mode"
|
||||
echo ""
|
||||
echo " Capabilities:"
|
||||
echo " • Join banned/invite-only/password/full channels"
|
||||
echo " • Cannot be kicked by anyone"
|
||||
echo " • Override all channel restrictions"
|
||||
echo ""
|
||||
|
||||
echo "👻 STEALTH MODE (+S):"
|
||||
echo " /mode mynick +S # Enable Stealth Mode - Invisible to regular users"
|
||||
echo " /mode mynick -S # Disable Stealth Mode"
|
||||
echo ""
|
||||
echo " Effects:"
|
||||
echo " • Hidden from /who and /names for regular users"
|
||||
echo " • Still visible to other operators"
|
||||
echo " • Invisible channel presence"
|
||||
echo ""
|
||||
|
||||
echo "⚡ COMBINED USAGE:"
|
||||
echo " /mode mynick +GS # Enable both modes simultaneously"
|
||||
echo " /mode mynick -GS # Disable both modes"
|
||||
echo ""
|
||||
|
||||
echo "📋 REQUIREMENTS:"
|
||||
echo " • Must be an IRC operator (/oper)"
|
||||
echo " • Operator class must have 'god_mode' and/or 'stealth_mode' permissions"
|
||||
echo " • Can only set modes on yourself"
|
||||
echo ""
|
||||
|
||||
echo "🛡️ PERMISSIONS:"
|
||||
echo " Add to your operator class in configs/opers.conf:"
|
||||
echo ' "permissions": ["*", "god_mode", "stealth_mode"]'
|
||||
echo ""
|
||||
|
||||
echo "🔍 CHECKING CURRENT MODES:"
|
||||
echo " /mode mynick # Show your current user modes"
|
||||
echo ""
|
||||
|
||||
echo "Example operator class with both permissions:"
|
||||
echo '{'
|
||||
echo ' "name": "admin",'
|
||||
echo ' "rank": 4,'
|
||||
echo ' "permissions": ["*", "god_mode", "stealth_mode"]'
|
||||
echo '}'
|
||||
echo ""
|
||||
|
||||
echo "This follows proper IRC conventions - user modes instead of custom commands!"
|
||||
echo "Use /mode +G and /mode +S just like any other IRC user mode."
|
||||
59
scripts/test_irc_connection.sh
Executable file
59
scripts/test_irc_connection.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Simple IRC client test to debug connection issues
|
||||
# This script connects to the IRC server and sends basic registration commands
|
||||
|
||||
echo "=== TechIRCd Connection Test ==="
|
||||
echo "Testing IRC registration sequence..."
|
||||
echo ""
|
||||
|
||||
# Create a temporary file for the test
|
||||
TEST_FILE="/tmp/irc_test.txt"
|
||||
|
||||
# Connect and send basic registration commands
|
||||
{
|
||||
echo "NICK testuser"
|
||||
echo "USER testuser 0 * :Test User"
|
||||
sleep 2
|
||||
echo "QUIT :Test complete"
|
||||
} | nc -q 3 127.0.0.1 6667 | tee "$TEST_FILE"
|
||||
|
||||
echo ""
|
||||
echo "=== Raw IRC Server Response ==="
|
||||
cat "$TEST_FILE"
|
||||
echo ""
|
||||
|
||||
echo "=== Analysis ==="
|
||||
if grep -q "001" "$TEST_FILE"; then
|
||||
echo "✅ RPL_WELCOME (001) found - Server sent welcome message"
|
||||
else
|
||||
echo "❌ RPL_WELCOME (001) NOT found - Registration may have failed"
|
||||
fi
|
||||
|
||||
if grep -q "002" "$TEST_FILE"; then
|
||||
echo "✅ RPL_YOURHOST (002) found"
|
||||
else
|
||||
echo "❌ RPL_YOURHOST (002) NOT found"
|
||||
fi
|
||||
|
||||
if grep -q "003" "$TEST_FILE"; then
|
||||
echo "✅ RPL_CREATED (003) found"
|
||||
else
|
||||
echo "❌ RPL_CREATED (003) NOT found"
|
||||
fi
|
||||
|
||||
if grep -q "004" "$TEST_FILE"; then
|
||||
echo "✅ RPL_MYINFO (004) found"
|
||||
else
|
||||
echo "❌ RPL_MYINFO (004) NOT found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "If the pyechat client connects but shows nothing, it might be:"
|
||||
echo "1. Not sending NICK/USER commands automatically"
|
||||
echo "2. Expecting different IRC numeric responses"
|
||||
echo "3. Not parsing the IRC protocol properly"
|
||||
echo "4. Waiting for specific server capabilities"
|
||||
|
||||
# Cleanup
|
||||
rm -f "$TEST_FILE"
|
||||
106
security_enhancements.go
Normal file
106
security_enhancements.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Advanced security and authentication
|
||||
type SecurityConfig struct {
|
||||
RateLimit struct {
|
||||
Enable bool `json:"enable"`
|
||||
MaxRequests int `json:"max_requests"`
|
||||
Window int `json:"window_seconds"`
|
||||
BanDuration int `json:"ban_duration"`
|
||||
} `json:"rate_limit"`
|
||||
|
||||
GeoBlocking struct {
|
||||
Enable bool `json:"enable"`
|
||||
Whitelist []string `json:"whitelist_countries"`
|
||||
Blacklist []string `json:"blacklist_countries"`
|
||||
} `json:"geo_blocking"`
|
||||
|
||||
TwoFactor struct {
|
||||
Enable bool `json:"enable"`
|
||||
Methods []string `json:"methods"` // totp, sms, email
|
||||
Required bool `json:"required_for_opers"`
|
||||
} `json:"two_factor"`
|
||||
|
||||
SASL struct {
|
||||
Enable bool `json:"enable"`
|
||||
Mechanisms []string `json:"mechanisms"` // PLAIN, EXTERNAL, SCRAM-SHA-256
|
||||
Required bool `json:"required"`
|
||||
} `json:"sasl"`
|
||||
}
|
||||
|
||||
// Rate limiting per IP/user
|
||||
type RateLimiter struct {
|
||||
connections map[string]*ConnectionLimit
|
||||
messages map[string]*MessageLimit
|
||||
}
|
||||
|
||||
type ConnectionLimit struct {
|
||||
IP net.IP
|
||||
Count int
|
||||
LastSeen time.Time
|
||||
Banned bool
|
||||
BanUntil time.Time
|
||||
}
|
||||
|
||||
type MessageLimit struct {
|
||||
Count int
|
||||
LastReset time.Time
|
||||
Violations int
|
||||
}
|
||||
|
||||
// Certificate-based authentication
|
||||
type CertAuth struct {
|
||||
Enable bool `json:"enable"`
|
||||
RequiredCAs []string `json:"required_cas"`
|
||||
UserMapping map[string]string `json:"user_mapping"` // cert fingerprint -> username
|
||||
AutoOper bool `json:"auto_oper"`
|
||||
}
|
||||
|
||||
// OAuth integration
|
||||
type OAuthConfig struct {
|
||||
Providers map[string]OAuthProvider `json:"providers"`
|
||||
}
|
||||
|
||||
type OAuthProvider struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
AuthURL string `json:"auth_url"`
|
||||
TokenURL string `json:"token_url"`
|
||||
UserInfoURL string `json:"user_info_url"`
|
||||
}
|
||||
|
||||
// DDoS protection
|
||||
type DDoSProtection struct {
|
||||
Enable bool `json:"enable"`
|
||||
MaxConnections int `json:"max_connections_per_ip"`
|
||||
ConnectionRate int `json:"max_connections_per_minute"`
|
||||
SynFloodProtection bool `json:"syn_flood_protection"`
|
||||
}
|
||||
|
||||
// Implement security features
|
||||
func (s *Server) CheckRateLimit(ip net.IP) bool {
|
||||
// Check if IP is rate limited
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) ValidateCertificate(cert *tls.Certificate) bool {
|
||||
// Validate client certificate
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) AuthenticateOAuth(provider, token string) (*UserInfo, error) {
|
||||
// OAuth authentication
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Username string
|
||||
Email string
|
||||
Verified bool
|
||||
}
|
||||
509
services.go
Normal file
509
services.go
Normal file
@@ -0,0 +1,509 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServicesManager handles all network services
|
||||
type ServicesManager struct {
|
||||
server *Server
|
||||
config *ServicesConfig
|
||||
nickServ *NickServ
|
||||
chanServ *ChanServ
|
||||
operServ *OperServ
|
||||
memoServ *MemoServ
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NickServ handles nickname registration and authentication
|
||||
type NickServ struct {
|
||||
manager *ServicesManager
|
||||
nick string
|
||||
accounts map[string]*NickAccount
|
||||
}
|
||||
|
||||
// ChanServ handles channel registration and management
|
||||
type ChanServ struct {
|
||||
manager *ServicesManager
|
||||
nick string
|
||||
channels map[string]*RegisteredChannel
|
||||
}
|
||||
|
||||
// OperServ handles network operator services
|
||||
type OperServ struct {
|
||||
manager *ServicesManager
|
||||
nick string
|
||||
}
|
||||
|
||||
// MemoServ handles user-to-user messages
|
||||
type MemoServ struct {
|
||||
manager *ServicesManager
|
||||
nick string
|
||||
memos map[string][]*Memo
|
||||
}
|
||||
|
||||
// NickAccount represents a registered nickname
|
||||
type NickAccount struct {
|
||||
Nick string `json:"nick"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
Email string `json:"email"`
|
||||
RegisterTime time.Time `json:"register_time"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
AccessList []string `json:"access_list"`
|
||||
Flags []string `json:"flags"`
|
||||
}
|
||||
|
||||
// RegisteredChannel represents a registered channel
|
||||
type RegisteredChannelServices struct {
|
||||
Name string `json:"name"`
|
||||
Founder string `json:"founder"`
|
||||
RegisterTime time.Time `json:"register_time"`
|
||||
Topic string `json:"topic"`
|
||||
Modes string `json:"modes"`
|
||||
AccessList map[string]*ChannelAccess `json:"access_list"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
AutoModes map[string]string `json:"auto_modes"`
|
||||
}
|
||||
|
||||
// ChannelAccess represents access levels for channels - using the one from database.go
|
||||
// type ChannelAccess struct {
|
||||
// Nick string `json:"nick"`
|
||||
// Level int `json:"level"` // 0=banned, 1=voice, 2=halfop, 3=op, 4=sop, 5=founder
|
||||
// SetBy string `json:"set_by"`
|
||||
// SetTime time.Time `json:"set_time"`
|
||||
// LastUsed time.Time `json:"last_used"`
|
||||
// }
|
||||
|
||||
// Memo represents a user memo
|
||||
type Memo struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Message string `json:"message"`
|
||||
Time time.Time `json:"time"`
|
||||
Read bool `json:"read"`
|
||||
}
|
||||
|
||||
// ServicesConfig configuration for services
|
||||
type ServicesConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
NickServ NickServConfig `json:"nickserv"`
|
||||
ChanServ ChanServConfig `json:"chanserv"`
|
||||
OperServ OperServConfig `json:"operserv"`
|
||||
MemoServ MemoServConfig `json:"memoserv"`
|
||||
Database DatabaseConfig `json:"database"`
|
||||
}
|
||||
|
||||
type NickServConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Nick string `json:"nick"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Realname string `json:"realname"`
|
||||
ExpireTime int `json:"expire_time"` // Days until unused nicks expire
|
||||
IdentifyTimeout int `json:"identify_timeout"` // Seconds to identify before kill
|
||||
EmailVerify bool `json:"email_verify"`
|
||||
RestrictReg bool `json:"restrict_registration"`
|
||||
}
|
||||
|
||||
type ChanServConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Nick string `json:"nick"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Realname string `json:"realname"`
|
||||
ExpireTime int `json:"expire_time"` // Days until unused channels expire
|
||||
MaxChannels int `json:"max_channels"` // Max channels per user
|
||||
AutoDeop bool `json:"auto_deop"` // Auto-deop users without access
|
||||
}
|
||||
|
||||
type OperServConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Nick string `json:"nick"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Realname string `json:"realname"`
|
||||
}
|
||||
|
||||
type MemoServConfig struct {
|
||||
Enable bool `json:"enable"`
|
||||
Nick string `json:"nick"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Realname string `json:"realname"`
|
||||
MaxMemos int `json:"max_memos"`
|
||||
MemoExpire int `json:"memo_expire"` // Days until memos expire
|
||||
}
|
||||
|
||||
// NewServicesManager creates a new services manager
|
||||
func NewServicesManager(server *Server, config *ServicesConfig) *ServicesManager {
|
||||
sm := &ServicesManager{
|
||||
server: server,
|
||||
config: config,
|
||||
enabled: config.Enable,
|
||||
}
|
||||
|
||||
if config.NickServ.Enable {
|
||||
sm.nickServ = &NickServ{
|
||||
manager: sm,
|
||||
nick: config.NickServ.Nick,
|
||||
accounts: make(map[string]*NickAccount),
|
||||
}
|
||||
}
|
||||
|
||||
if config.ChanServ.Enable {
|
||||
sm.chanServ = &ChanServ{
|
||||
manager: sm,
|
||||
nick: config.ChanServ.Nick,
|
||||
channels: make(map[string]*RegisteredChannel),
|
||||
}
|
||||
}
|
||||
|
||||
if config.OperServ.Enable {
|
||||
sm.operServ = &OperServ{
|
||||
manager: sm,
|
||||
nick: config.OperServ.Nick,
|
||||
}
|
||||
}
|
||||
|
||||
if config.MemoServ.Enable {
|
||||
sm.memoServ = &MemoServ{
|
||||
manager: sm,
|
||||
nick: config.MemoServ.Nick,
|
||||
memos: make(map[string][]*Memo),
|
||||
}
|
||||
}
|
||||
|
||||
return sm
|
||||
}
|
||||
|
||||
// Start initializes and starts all enabled services
|
||||
func (sm *ServicesManager) Start() error {
|
||||
if !sm.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Starting TechIRCd Services...")
|
||||
|
||||
// Create service clients
|
||||
if sm.nickServ != nil {
|
||||
sm.createServiceClient(sm.config.NickServ.Nick, sm.config.NickServ.User,
|
||||
sm.config.NickServ.Host, sm.config.NickServ.Realname)
|
||||
}
|
||||
|
||||
if sm.chanServ != nil {
|
||||
sm.createServiceClient(sm.config.ChanServ.Nick, sm.config.ChanServ.User,
|
||||
sm.config.ChanServ.Host, sm.config.ChanServ.Realname)
|
||||
}
|
||||
|
||||
if sm.operServ != nil {
|
||||
sm.createServiceClient(sm.config.OperServ.Nick, sm.config.OperServ.User,
|
||||
sm.config.OperServ.Host, sm.config.OperServ.Realname)
|
||||
}
|
||||
|
||||
if sm.memoServ != nil {
|
||||
sm.createServiceClient(sm.config.MemoServ.Nick, sm.config.MemoServ.User,
|
||||
sm.config.MemoServ.Host, sm.config.MemoServ.Realname)
|
||||
}
|
||||
|
||||
log.Println("Services started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// createServiceClient creates a virtual client for a service
|
||||
func (sm *ServicesManager) createServiceClient(nick, user, host, realname string) {
|
||||
// Create a virtual service client that appears as a regular user
|
||||
serviceClient := &Client{
|
||||
nick: nick,
|
||||
user: user,
|
||||
host: host,
|
||||
realname: realname,
|
||||
server: sm.server,
|
||||
modes: make(map[rune]bool),
|
||||
channels: make(map[string]*Channel),
|
||||
capabilities: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Set service modes
|
||||
serviceClient.modes['S'] = true // Service mode
|
||||
serviceClient.modes['o'] = true // Operator mode
|
||||
|
||||
// Add to server
|
||||
sm.server.clients[strings.ToLower(nick)] = serviceClient
|
||||
|
||||
// Send introduction to network
|
||||
sm.server.BroadcastToServers(fmt.Sprintf(":%s NICK %s", sm.server.config.Server.Name, nick))
|
||||
}
|
||||
|
||||
// HandleMessage processes messages directed to services
|
||||
func (sm *ServicesManager) HandleMessage(from *Client, target, message string) bool {
|
||||
if !sm.enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
lowerTarget := strings.ToLower(target)
|
||||
parts := strings.Fields(message)
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
command := strings.ToUpper(parts[0])
|
||||
|
||||
switch lowerTarget {
|
||||
case strings.ToLower(sm.config.NickServ.Nick):
|
||||
return sm.handleNickServCommand(from, command, parts[1:])
|
||||
case strings.ToLower(sm.config.ChanServ.Nick):
|
||||
return sm.handleChanServCommand(from)
|
||||
case strings.ToLower(sm.config.OperServ.Nick):
|
||||
return sm.handleOperServCommand(from)
|
||||
case strings.ToLower(sm.config.MemoServ.Nick):
|
||||
return sm.handleMemoServCommand(from)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NickServ command handlers
|
||||
func (sm *ServicesManager) handleNickServCommand(from *Client, command string, args []string) bool {
|
||||
switch command {
|
||||
case "REGISTER":
|
||||
return sm.handleNickServRegister(from, args)
|
||||
case "IDENTIFY":
|
||||
return sm.handleNickServIdentify(from, args)
|
||||
case "INFO":
|
||||
return sm.handleNickServInfo(from, args)
|
||||
case "DROP":
|
||||
return sm.handleNickServDrop(from, args)
|
||||
case "SET":
|
||||
return sm.handleNickServSet(from)
|
||||
case "ACCESS":
|
||||
return sm.handleNickServAccess(from)
|
||||
case "HELP":
|
||||
return sm.handleNickServHelp(from)
|
||||
default:
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("Unknown command %s. Type /msg %s HELP for help.", command, sm.config.NickServ.Nick))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServRegister(from *Client, args []string) bool {
|
||||
if len(args) < 2 {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"Syntax: REGISTER <password> <email>")
|
||||
return true
|
||||
}
|
||||
|
||||
nick := strings.ToLower(from.nick)
|
||||
password := args[0]
|
||||
email := args[1]
|
||||
|
||||
// Check if nick is already registered
|
||||
if _, exists := sm.nickServ.accounts[nick]; exists {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"This nickname is already registered.")
|
||||
return true
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
passwordHash := hex.EncodeToString(hash[:])
|
||||
|
||||
// Create account
|
||||
account := &NickAccount{
|
||||
Nick: from.nick,
|
||||
PasswordHash: passwordHash,
|
||||
Email: email,
|
||||
RegisterTime: time.Now(),
|
||||
LastSeen: time.Now(),
|
||||
Settings: make(map[string]string),
|
||||
AccessList: []string{},
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
sm.nickServ.accounts[nick] = account
|
||||
|
||||
// Set user as identified
|
||||
from.account = from.nick
|
||||
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("Nickname %s has been registered successfully.", from.nick))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServIdentify(from *Client, args []string) bool {
|
||||
if len(args) < 1 {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"Syntax: IDENTIFY <password>")
|
||||
return true
|
||||
}
|
||||
|
||||
nick := strings.ToLower(from.nick)
|
||||
password := args[0]
|
||||
|
||||
account, exists := sm.nickServ.accounts[nick]
|
||||
if !exists {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"This nickname is not registered.")
|
||||
return true
|
||||
}
|
||||
|
||||
// Check password
|
||||
hash := sha256.Sum256([]byte(password))
|
||||
passwordHash := hex.EncodeToString(hash[:])
|
||||
|
||||
if account.PasswordHash != passwordHash {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"Invalid password.")
|
||||
return true
|
||||
}
|
||||
|
||||
// Set user as identified
|
||||
from.account = from.nick
|
||||
account.LastSeen = time.Now()
|
||||
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("You are now identified for %s.", from.nick))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServInfo(from *Client, args []string) bool {
|
||||
var targetNick string
|
||||
if len(args) > 0 {
|
||||
targetNick = args[0]
|
||||
} else {
|
||||
targetNick = from.nick
|
||||
}
|
||||
|
||||
nick := strings.ToLower(targetNick)
|
||||
account, exists := sm.nickServ.accounts[nick]
|
||||
if !exists {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("The nickname %s is not registered.", targetNick))
|
||||
return true
|
||||
}
|
||||
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("Information for %s:", account.Nick))
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("Registered: %s", account.RegisterTime.Format("Jan 02, 2006 15:04:05 MST")))
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("Last seen: %s", account.LastSeen.Format("Jan 02, 2006 15:04:05 MST")))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServDrop(from *Client, _ []string) bool {
|
||||
nick := strings.ToLower(from.nick)
|
||||
|
||||
if from.account != from.nick {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"You must be identified to drop your nickname.")
|
||||
return true
|
||||
}
|
||||
|
||||
delete(sm.nickServ.accounts, nick)
|
||||
from.account = ""
|
||||
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
fmt.Sprintf("Nickname %s has been dropped.", from.nick))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServSet(from *Client) bool {
|
||||
// Handle SET commands (EMAIL, PASSWORD, etc.)
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"SET command not yet implemented.")
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServAccess(from *Client) bool {
|
||||
// Handle ACCESS commands (ADD, DEL, LIST)
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
||||
"ACCESS command not yet implemented.")
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *ServicesManager) handleNickServHelp(from *Client) bool {
|
||||
help := []string{
|
||||
"*** NickServ Help ***",
|
||||
"REGISTER <password> <email> - Register your nickname",
|
||||
"IDENTIFY <password> - Identify to your nickname",
|
||||
"INFO [nick] - Show information about a nickname",
|
||||
"DROP - Drop your nickname registration",
|
||||
"SET - Change settings",
|
||||
"ACCESS - Manage access list",
|
||||
"HELP - Show this help",
|
||||
}
|
||||
|
||||
for _, line := range help {
|
||||
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick, line)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ChanServ command handlers (placeholder)
|
||||
func (sm *ServicesManager) handleChanServCommand(from *Client) bool {
|
||||
sm.sendServiceNotice(sm.config.ChanServ.Nick, from.nick,
|
||||
"ChanServ commands not yet implemented.")
|
||||
return true
|
||||
}
|
||||
|
||||
// OperServ command handlers (placeholder)
|
||||
func (sm *ServicesManager) handleOperServCommand(from *Client) bool {
|
||||
sm.sendServiceNotice(sm.config.OperServ.Nick, from.nick,
|
||||
"OperServ commands not yet implemented.")
|
||||
return true
|
||||
}
|
||||
|
||||
// MemoServ command handlers (placeholder)
|
||||
func (sm *ServicesManager) handleMemoServCommand(from *Client) bool {
|
||||
sm.sendServiceNotice(sm.config.MemoServ.Nick, from.nick,
|
||||
"MemoServ commands not yet implemented.")
|
||||
return true
|
||||
}
|
||||
|
||||
// sendServiceNotice sends a notice from a service to a user
|
||||
func (sm *ServicesManager) sendServiceNotice(from, to, message string) {
|
||||
if client := sm.server.GetClient(to); client != nil {
|
||||
client.SendMessage(fmt.Sprintf(":%s NOTICE %s :%s", from, to, message))
|
||||
}
|
||||
}
|
||||
|
||||
// IsServiceNick checks if a nick is a service
|
||||
func (sm *ServicesManager) IsServiceNick(nick string) bool {
|
||||
if !sm.enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
lowerNick := strings.ToLower(nick)
|
||||
return lowerNick == strings.ToLower(sm.config.NickServ.Nick) ||
|
||||
lowerNick == strings.ToLower(sm.config.ChanServ.Nick) ||
|
||||
lowerNick == strings.ToLower(sm.config.OperServ.Nick) ||
|
||||
lowerNick == strings.ToLower(sm.config.MemoServ.Nick)
|
||||
}
|
||||
|
||||
// GetUserAccount returns the account for a nick
|
||||
func (sm *ServicesManager) GetUserAccount(nick string) *NickAccount {
|
||||
if sm.nickServ == nil {
|
||||
return nil
|
||||
}
|
||||
return sm.nickServ.accounts[strings.ToLower(nick)]
|
||||
}
|
||||
|
||||
// IsIdentified checks if a user is identified
|
||||
func (sm *ServicesManager) IsIdentified(nick string) bool {
|
||||
if client := sm.server.GetClient(nick); client != nil {
|
||||
return client.account != ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
309
stress_config.json
Normal file
309
stress_config.json
Normal file
@@ -0,0 +1,309 @@
|
||||
{
|
||||
"server": {
|
||||
"host": "localhost",
|
||||
"port": 6667,
|
||||
"ssl": false,
|
||||
"ssl_port": 6697,
|
||||
"nick_prefix": "StressBot",
|
||||
"auto_join_channels": ["#test", "#general"]
|
||||
},
|
||||
"log_level": "INFO",
|
||||
"scenarios": [
|
||||
{
|
||||
"name": "Basic Connection Test",
|
||||
"description": "Test basic connection and registration",
|
||||
"client_count": 10,
|
||||
"duration": 30,
|
||||
"connect_gradually": false,
|
||||
"disconnect_gradually": true,
|
||||
"disconnect_delay": 0.2,
|
||||
"delay_after": 5,
|
||||
"activities": []
|
||||
},
|
||||
{
|
||||
"name": "Mass Connection Stress",
|
||||
"description": "Stress test with many simultaneous connections",
|
||||
"client_count": 50,
|
||||
"duration": 60,
|
||||
"connect_gradually": false,
|
||||
"disconnect_gradually": false,
|
||||
"delay_after": 5,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#test",
|
||||
"message_count": 20,
|
||||
"delay": 2.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Gradual Connection Test",
|
||||
"description": "Test gradual connection buildup",
|
||||
"client_count": 30,
|
||||
"duration": 45,
|
||||
"connect_gradually": true,
|
||||
"connect_delay": 0.1,
|
||||
"disconnect_gradually": true,
|
||||
"disconnect_delay": 0.1,
|
||||
"delay_after": 3,
|
||||
"activities": [
|
||||
{
|
||||
"type": "private_flood",
|
||||
"message_count": 15,
|
||||
"delay": 3.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Heavy Activity Test",
|
||||
"description": "Test server under heavy message load",
|
||||
"client_count": 25,
|
||||
"duration": 90,
|
||||
"connect_gradually": false,
|
||||
"disconnect_gradually": false,
|
||||
"delay_after": 5,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#test",
|
||||
"message_count": 30,
|
||||
"delay": 1.0
|
||||
},
|
||||
{
|
||||
"type": "private_flood",
|
||||
"message_count": 20,
|
||||
"delay": 1.5
|
||||
},
|
||||
{
|
||||
"type": "join_part_spam",
|
||||
"iterations": 10,
|
||||
"channels": ["#spam1", "#spam2", "#spam3"],
|
||||
"delay": 2.0
|
||||
},
|
||||
{
|
||||
"type": "random_commands",
|
||||
"command_count": 15,
|
||||
"commands": ["WHO #test", "LIST", "VERSION", "WHOIS StressBot0001"],
|
||||
"delay": 2.5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nick Change Spam",
|
||||
"description": "Test rapid nick changes",
|
||||
"client_count": 15,
|
||||
"duration": 30,
|
||||
"connect_gradually": false,
|
||||
"disconnect_gradually": false,
|
||||
"delay_after": 3,
|
||||
"activities": [
|
||||
{
|
||||
"type": "nick_change_spam",
|
||||
"iterations": 20,
|
||||
"delay": 1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Channel Chaos Test",
|
||||
"description": "Rapid JOIN/PART with message flooding",
|
||||
"client_count": 20,
|
||||
"duration": 60,
|
||||
"connect_gradually": true,
|
||||
"connect_delay": 0.05,
|
||||
"disconnect_gradually": true,
|
||||
"disconnect_delay": 0.05,
|
||||
"delay_after": 5,
|
||||
"activities": [
|
||||
{
|
||||
"type": "join_part_spam",
|
||||
"iterations": 25,
|
||||
"channels": ["#chaos1", "#chaos2", "#chaos3", "#chaos4"],
|
||||
"delay": 0.5
|
||||
},
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#chaos1",
|
||||
"message_count": 40,
|
||||
"delay": 1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Maximum Load Test",
|
||||
"description": "Push server to maximum capacity",
|
||||
"client_count": 100,
|
||||
"duration": 120,
|
||||
"connect_gradually": true,
|
||||
"connect_delay": 0.02,
|
||||
"disconnect_gradually": true,
|
||||
"disconnect_delay": 0.01,
|
||||
"delay_after": 10,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#maxload",
|
||||
"message_count": 50,
|
||||
"delay": 0.8
|
||||
},
|
||||
{
|
||||
"type": "private_flood",
|
||||
"message_count": 30,
|
||||
"delay": 1.2
|
||||
},
|
||||
{
|
||||
"type": "join_part_spam",
|
||||
"iterations": 15,
|
||||
"channels": ["#load1", "#load2", "#load3", "#load4", "#load5"],
|
||||
"delay": 1.5
|
||||
},
|
||||
{
|
||||
"type": "random_commands",
|
||||
"command_count": 25,
|
||||
"commands": ["WHO #maxload", "LIST", "VERSION", "WHOIS StressBot0050", "NAMES #load1"],
|
||||
"delay": 2.0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EXTREME: Mass Connection Bomb",
|
||||
"description": "BRUTAL: 200+ connections at once",
|
||||
"client_count": 200,
|
||||
"duration": 120,
|
||||
"connect_gradually": false,
|
||||
"disconnect_gradually": false,
|
||||
"delay_after": 10,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#massacre",
|
||||
"message_count": 50,
|
||||
"delay": 0.1
|
||||
},
|
||||
{
|
||||
"type": "private_flood",
|
||||
"message_count": 30,
|
||||
"delay": 0.2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EXTREME: Rapid Fire Connections",
|
||||
"description": "BRUTAL: Connect 150 clients as fast as possible",
|
||||
"client_count": 150,
|
||||
"duration": 60,
|
||||
"connect_gradually": true,
|
||||
"connect_delay": 0.001,
|
||||
"disconnect_gradually": true,
|
||||
"disconnect_delay": 0.001,
|
||||
"delay_after": 5,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#rapidfire",
|
||||
"message_count": 100,
|
||||
"delay": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EXTREME: Message Hurricane",
|
||||
"description": "BRUTAL: Maximum message throughput test",
|
||||
"client_count": 80,
|
||||
"duration": 180,
|
||||
"connect_gradually": false,
|
||||
"disconnect_gradually": false,
|
||||
"delay_after": 10,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#hurricane",
|
||||
"message_count": 200,
|
||||
"delay": 0.01
|
||||
},
|
||||
{
|
||||
"type": "private_flood",
|
||||
"message_count": 150,
|
||||
"delay": 0.02
|
||||
},
|
||||
{
|
||||
"type": "join_part_spam",
|
||||
"iterations": 50,
|
||||
"channels": ["#chaos1", "#chaos2", "#chaos3", "#chaos4", "#chaos5"],
|
||||
"delay": 0.1
|
||||
},
|
||||
{
|
||||
"type": "random_commands",
|
||||
"command_count": 100,
|
||||
"commands": ["WHO #hurricane", "LIST", "VERSION", "TIME", "ADMIN", "INFO"],
|
||||
"delay": 0.05
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EXTREME: Connection Chaos",
|
||||
"description": "BRUTAL: Rapid connect/disconnect cycles",
|
||||
"client_count": 100,
|
||||
"duration": 90,
|
||||
"connect_gradually": true,
|
||||
"connect_delay": 0.01,
|
||||
"disconnect_gradually": true,
|
||||
"disconnect_delay": 0.01,
|
||||
"delay_after": 5,
|
||||
"activities": [
|
||||
{
|
||||
"type": "nick_spam",
|
||||
"iterations": 20,
|
||||
"delay": 0.1
|
||||
},
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#chaos",
|
||||
"message_count": 75,
|
||||
"delay": 0.1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "EXTREME: The Nuclear Option",
|
||||
"description": "ULTIMATE BRUTAL: Everything at maximum intensity",
|
||||
"client_count": 300,
|
||||
"duration": 300,
|
||||
"connect_gradually": true,
|
||||
"connect_delay": 0.002,
|
||||
"disconnect_gradually": false,
|
||||
"delay_after": 15,
|
||||
"activities": [
|
||||
{
|
||||
"type": "channel_flood",
|
||||
"channel": "#nuclear",
|
||||
"message_count": 500,
|
||||
"delay": 0.005
|
||||
},
|
||||
{
|
||||
"type": "private_flood",
|
||||
"message_count": 300,
|
||||
"delay": 0.01
|
||||
},
|
||||
{
|
||||
"type": "join_part_spam",
|
||||
"iterations": 100,
|
||||
"channels": ["#nuke1", "#nuke2", "#nuke3", "#nuke4", "#nuke5", "#nuke6", "#nuke7", "#nuke8"],
|
||||
"delay": 0.02
|
||||
},
|
||||
{
|
||||
"type": "nick_spam",
|
||||
"iterations": 50,
|
||||
"delay": 0.03
|
||||
},
|
||||
{
|
||||
"type": "random_commands",
|
||||
"command_count": 200,
|
||||
"commands": ["WHO #nuclear", "LIST", "VERSION", "TIME", "ADMIN", "INFO", "LUSERS"],
|
||||
"delay": 0.01
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
453
stress_test.py
Normal file
453
stress_test.py
Normal file
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TechIRCd Stress Testing Tool
|
||||
Advanced IRC server stress testing with configurable scenarios
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
from typing import List, Dict, Any
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
class IRCClient:
|
||||
def __init__(self, config: Dict[str, Any], client_id: int):
|
||||
self.config = config
|
||||
self.client_id = client_id
|
||||
self.nick = f"{config['nick_prefix']}{client_id:04d}"
|
||||
self.user = f"user{client_id}"
|
||||
self.realname = f"Stress Test Client {client_id}"
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.connected = False
|
||||
self.registered = False
|
||||
self.channels = []
|
||||
self.message_count = 0
|
||||
self.start_time = None
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to IRC server"""
|
||||
try:
|
||||
if self.config.get('ssl', False):
|
||||
context = ssl.create_default_context()
|
||||
self.reader, self.writer = await asyncio.open_connection(
|
||||
self.config['host'],
|
||||
self.config.get('ssl_port', 6697),
|
||||
ssl=context
|
||||
)
|
||||
else:
|
||||
self.reader, self.writer = await asyncio.open_connection(
|
||||
self.config['host'],
|
||||
self.config['port']
|
||||
)
|
||||
|
||||
self.connected = True
|
||||
self.start_time = time.time()
|
||||
|
||||
# Start registration
|
||||
await self.send(f"NICK {self.nick}")
|
||||
await self.send(f"USER {self.user} 0 * :{self.realname}")
|
||||
|
||||
# Start message handler
|
||||
asyncio.create_task(self.message_handler())
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} connection failed: {e}")
|
||||
return False
|
||||
|
||||
async def send(self, message: str):
|
||||
"""Send message to server"""
|
||||
if self.writer and not self.writer.is_closing():
|
||||
try:
|
||||
self.writer.write(f"{message}\r\n".encode())
|
||||
await self.writer.drain()
|
||||
logging.debug(f"Client {self.client_id} sent: {message}")
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} send error: {e}")
|
||||
|
||||
async def message_handler(self):
|
||||
"""Handle incoming messages"""
|
||||
try:
|
||||
while self.connected and self.reader:
|
||||
line = await self.reader.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
message = line.decode().strip()
|
||||
if not message:
|
||||
continue
|
||||
|
||||
logging.debug(f"Client {self.client_id} received: {message}")
|
||||
await self.handle_message(message)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} message handler error: {e}")
|
||||
finally:
|
||||
self.connected = False
|
||||
|
||||
async def handle_message(self, message: str):
|
||||
"""Process incoming IRC messages"""
|
||||
parts = message.split()
|
||||
if len(parts) < 2:
|
||||
return
|
||||
|
||||
# Handle PING
|
||||
if parts[0] == "PING":
|
||||
await self.send(f"PONG {parts[1]}")
|
||||
return
|
||||
|
||||
# Handle numeric responses
|
||||
if len(parts) >= 2 and parts[1].isdigit():
|
||||
numeric = int(parts[1])
|
||||
|
||||
# Welcome message - registration complete
|
||||
if numeric == 1: # RPL_WELCOME
|
||||
self.registered = True
|
||||
logging.info(f"Client {self.client_id} ({self.nick}) registered successfully")
|
||||
|
||||
# Auto-join channels if configured
|
||||
for channel in self.config.get('auto_join_channels', []):
|
||||
await self.join_channel(channel)
|
||||
|
||||
async def join_channel(self, channel: str):
|
||||
"""Join a channel"""
|
||||
await self.send(f"JOIN {channel}")
|
||||
if channel not in self.channels:
|
||||
self.channels.append(channel)
|
||||
|
||||
async def send_privmsg(self, target: str, message: str):
|
||||
"""Send private message or channel message"""
|
||||
await self.send(f"PRIVMSG {target} :{message}")
|
||||
self.message_count += 1
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from server"""
|
||||
self.connected = False
|
||||
if self.writer and not self.writer.is_closing():
|
||||
await self.send("QUIT :Stress test complete")
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
|
||||
class StressTest:
|
||||
def __init__(self, config_file: str):
|
||||
with open(config_file, 'r') as f:
|
||||
self.config = json.load(f)
|
||||
|
||||
self.clients: List[IRCClient] = []
|
||||
self.start_time = None
|
||||
self.stats = {
|
||||
'connected': 0,
|
||||
'registered': 0,
|
||||
'messages_sent': 0,
|
||||
'errors': 0
|
||||
}
|
||||
|
||||
# Setup logging
|
||||
log_level = getattr(logging, self.config.get('log_level', 'INFO').upper())
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('stress_test.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
async def run_scenario(self, scenario: Dict[str, Any]):
|
||||
"""Run a specific test scenario"""
|
||||
scenario_name = scenario['name']
|
||||
client_count = scenario['client_count']
|
||||
duration = scenario.get('duration', 60)
|
||||
|
||||
logging.info(f"Starting scenario: {scenario_name}")
|
||||
logging.info(f"Clients: {client_count}, Duration: {duration}s")
|
||||
|
||||
# Create and connect clients
|
||||
if scenario.get('connect_gradually', False):
|
||||
await self.gradual_connect(client_count, scenario.get('connect_delay', 0.1))
|
||||
else:
|
||||
await self.mass_connect(client_count)
|
||||
|
||||
# Run scenario activities
|
||||
await self.run_activities(scenario, duration)
|
||||
|
||||
# Collect stats
|
||||
await self.collect_stats()
|
||||
|
||||
# Disconnect clients
|
||||
if scenario.get('disconnect_gradually', False):
|
||||
await self.gradual_disconnect(scenario.get('disconnect_delay', 0.1))
|
||||
else:
|
||||
await self.mass_disconnect()
|
||||
|
||||
logging.info(f"Scenario {scenario_name} completed")
|
||||
|
||||
async def mass_connect(self, count: int):
|
||||
"""Connect many clients simultaneously"""
|
||||
logging.info(f"Mass connecting {count} clients...")
|
||||
|
||||
tasks = []
|
||||
for i in range(count):
|
||||
client = IRCClient(self.config['server'], i + 1)
|
||||
self.clients.append(client)
|
||||
tasks.append(client.connect())
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
connected = sum(1 for r in results if r is True)
|
||||
self.stats['connected'] = connected
|
||||
|
||||
logging.info(f"Connected {connected}/{count} clients")
|
||||
|
||||
# Wait for registration
|
||||
await asyncio.sleep(2)
|
||||
|
||||
registered = sum(1 for c in self.clients if c.registered)
|
||||
self.stats['registered'] = registered
|
||||
logging.info(f"Registered {registered}/{connected} clients")
|
||||
|
||||
async def gradual_connect(self, count: int, delay: float):
|
||||
"""Connect clients gradually with delay"""
|
||||
logging.info(f"Gradually connecting {count} clients with {delay}s delay...")
|
||||
|
||||
for i in range(count):
|
||||
client = IRCClient(self.config['server'], i + 1)
|
||||
self.clients.append(client)
|
||||
|
||||
success = await client.connect()
|
||||
if success:
|
||||
self.stats['connected'] += 1
|
||||
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# Wait for registration
|
||||
await asyncio.sleep(2)
|
||||
|
||||
registered = sum(1 for c in self.clients if c.registered)
|
||||
self.stats['registered'] = registered
|
||||
logging.info(f"Registered {registered}/{self.stats['connected']} clients")
|
||||
|
||||
async def run_activities(self, scenario: Dict[str, Any], duration: int):
|
||||
"""Run scenario activities for specified duration"""
|
||||
activities = scenario.get('activities', [])
|
||||
if not activities:
|
||||
logging.info(f"No activities specified, waiting {duration} seconds...")
|
||||
await asyncio.sleep(duration)
|
||||
return
|
||||
|
||||
end_time = time.time() + duration
|
||||
|
||||
while time.time() < end_time:
|
||||
for activity in activities:
|
||||
if time.time() >= end_time:
|
||||
break
|
||||
|
||||
await self.run_activity(activity)
|
||||
|
||||
# Delay between activities
|
||||
delay = activity.get('delay', 1.0)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
async def run_activity(self, activity: Dict[str, Any]):
|
||||
"""Run a single activity"""
|
||||
activity_type = activity['type']
|
||||
|
||||
if activity_type == 'channel_flood':
|
||||
await self.channel_flood_activity(activity)
|
||||
elif activity_type == 'private_flood':
|
||||
await self.private_flood_activity(activity)
|
||||
elif activity_type == 'join_part_spam':
|
||||
await self.join_part_spam_activity(activity)
|
||||
elif activity_type == 'nick_change_spam':
|
||||
await self.nick_change_spam_activity(activity)
|
||||
elif activity_type == 'random_commands':
|
||||
await self.random_commands_activity(activity)
|
||||
else:
|
||||
logging.warning(f"Unknown activity type: {activity_type}")
|
||||
|
||||
async def channel_flood_activity(self, activity: Dict[str, Any]):
|
||||
"""Flood channels with messages"""
|
||||
message_count = activity.get('message_count', 10)
|
||||
channel = activity.get('channel', '#test')
|
||||
|
||||
registered_clients = [c for c in self.clients if c.registered]
|
||||
if not registered_clients:
|
||||
return
|
||||
|
||||
tasks = []
|
||||
for _ in range(message_count):
|
||||
client = random.choice(registered_clients)
|
||||
message = self.generate_random_message()
|
||||
tasks.append(client.send_privmsg(channel, message))
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
self.stats['messages_sent'] += len(tasks)
|
||||
|
||||
async def private_flood_activity(self, activity: Dict[str, Any]):
|
||||
"""Flood with private messages"""
|
||||
message_count = activity.get('message_count', 10)
|
||||
|
||||
registered_clients = [c for c in self.clients if c.registered]
|
||||
if len(registered_clients) < 2:
|
||||
return
|
||||
|
||||
tasks = []
|
||||
for _ in range(message_count):
|
||||
sender = random.choice(registered_clients)
|
||||
target = random.choice(registered_clients)
|
||||
if sender != target:
|
||||
message = self.generate_random_message()
|
||||
tasks.append(sender.send_privmsg(target.nick, message))
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
self.stats['messages_sent'] += len(tasks)
|
||||
|
||||
async def join_part_spam_activity(self, activity: Dict[str, Any]):
|
||||
"""Spam JOIN/PART commands"""
|
||||
iterations = activity.get('iterations', 5)
|
||||
channels = activity.get('channels', ['#spam1', '#spam2', '#spam3'])
|
||||
|
||||
registered_clients = [c for c in self.clients if c.registered]
|
||||
if not registered_clients:
|
||||
return
|
||||
|
||||
for _ in range(iterations):
|
||||
client = random.choice(registered_clients)
|
||||
channel = random.choice(channels)
|
||||
|
||||
await client.join_channel(channel)
|
||||
await asyncio.sleep(0.1)
|
||||
await client.send(f"PART {channel} :Spam test")
|
||||
|
||||
async def nick_change_spam_activity(self, activity: Dict[str, Any]):
|
||||
"""Spam NICK changes"""
|
||||
iterations = activity.get('iterations', 5)
|
||||
|
||||
registered_clients = [c for c in self.clients if c.registered]
|
||||
if not registered_clients:
|
||||
return
|
||||
|
||||
for _ in range(iterations):
|
||||
client = random.choice(registered_clients)
|
||||
new_nick = f"{client.nick}_{''.join(random.choices(string.ascii_lowercase, k=3))}"
|
||||
await client.send(f"NICK {new_nick}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def random_commands_activity(self, activity: Dict[str, Any]):
|
||||
"""Send random IRC commands"""
|
||||
command_count = activity.get('command_count', 10)
|
||||
commands = activity.get('commands', ['WHO #test', 'WHOIS randomnick', 'LIST', 'VERSION'])
|
||||
|
||||
registered_clients = [c for c in self.clients if c.registered]
|
||||
if not registered_clients:
|
||||
return
|
||||
|
||||
for _ in range(command_count):
|
||||
client = random.choice(registered_clients)
|
||||
command = random.choice(commands)
|
||||
await client.send(command)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
def generate_random_message(self) -> str:
|
||||
"""Generate random message content"""
|
||||
words = ['hello', 'world', 'test', 'stress', 'irc', 'server', 'message', 'random', 'data']
|
||||
return ' '.join(random.choices(words, k=random.randint(1, 8)))
|
||||
|
||||
async def collect_stats(self):
|
||||
"""Collect and display statistics"""
|
||||
connected = sum(1 for c in self.clients if c.connected)
|
||||
registered = sum(1 for c in self.clients if c.registered)
|
||||
total_messages = sum(c.message_count for c in self.clients)
|
||||
|
||||
self.stats.update({
|
||||
'connected': connected,
|
||||
'registered': registered,
|
||||
'messages_sent': total_messages
|
||||
})
|
||||
|
||||
logging.info(f"Stats: {connected} connected, {registered} registered, {total_messages} messages sent")
|
||||
|
||||
async def mass_disconnect(self):
|
||||
"""Disconnect all clients simultaneously"""
|
||||
logging.info("Disconnecting all clients...")
|
||||
|
||||
tasks = [client.disconnect() for client in self.clients]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
self.clients.clear()
|
||||
|
||||
async def gradual_disconnect(self, delay: float):
|
||||
"""Disconnect clients gradually"""
|
||||
logging.info(f"Gradually disconnecting clients with {delay}s delay...")
|
||||
|
||||
for client in self.clients:
|
||||
await client.disconnect()
|
||||
if delay > 0:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
self.clients.clear()
|
||||
|
||||
async def run_all_scenarios(self):
|
||||
"""Run all configured scenarios"""
|
||||
self.start_time = time.time()
|
||||
|
||||
for scenario in self.config['scenarios']:
|
||||
try:
|
||||
await self.run_scenario(scenario)
|
||||
|
||||
# Delay between scenarios
|
||||
scenario_delay = scenario.get('delay_after', 2)
|
||||
if scenario_delay > 0:
|
||||
logging.info(f"Waiting {scenario_delay}s before next scenario...")
|
||||
await asyncio.sleep(scenario_delay)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Scenario {scenario['name']} failed: {e}")
|
||||
self.stats['errors'] += 1
|
||||
|
||||
total_time = time.time() - self.start_time
|
||||
logging.info(f"All scenarios completed in {total_time:.2f} seconds")
|
||||
logging.info(f"Final stats: {self.stats}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='TechIRCd Stress Testing Tool')
|
||||
parser.add_argument('--config', '-c', default='stress_config.json',
|
||||
help='Configuration file (default: stress_config.json)')
|
||||
parser.add_argument('--scenario', '-s', help='Run specific scenario only')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
stress_test = StressTest(args.config)
|
||||
|
||||
if args.scenario:
|
||||
# Run specific scenario
|
||||
scenario = next((s for s in stress_test.config['scenarios'] if s['name'] == args.scenario), None)
|
||||
if scenario:
|
||||
asyncio.run(stress_test.run_scenario(scenario))
|
||||
else:
|
||||
print(f"Scenario '{args.scenario}' not found")
|
||||
return 1
|
||||
else:
|
||||
# Run all scenarios
|
||||
asyncio.run(stress_test.run_all_scenarios())
|
||||
|
||||
return 0
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Configuration file '{args.config}' not found")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
154
test_ban_enforcement.py
Normal file
154
test_ban_enforcement.py
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
|
||||
def connect_client(nickname):
|
||||
"""Connect a client and return the socket"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(('localhost', 6667))
|
||||
|
||||
# Send initial IRC commands
|
||||
sock.send(f"NICK {nickname}\r\n".encode())
|
||||
sock.send(f"USER {nickname} 0 * :Test User\r\n".encode())
|
||||
|
||||
# Wait for welcome messages
|
||||
time.sleep(0.5)
|
||||
|
||||
return sock
|
||||
except Exception as e:
|
||||
print(f"Error connecting {nickname}: {e}")
|
||||
return None
|
||||
|
||||
def read_responses(sock, name, duration=2):
|
||||
"""Read responses from server for a duration"""
|
||||
try:
|
||||
sock.settimeout(0.1)
|
||||
responses = []
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < duration:
|
||||
try:
|
||||
data = sock.recv(4096).decode()
|
||||
if data:
|
||||
responses.append(data.strip())
|
||||
print(f"[{name}] {data.strip()}")
|
||||
except socket.timeout:
|
||||
continue
|
||||
except:
|
||||
break
|
||||
|
||||
return responses
|
||||
except Exception as e:
|
||||
print(f"Error reading from {name}: {e}")
|
||||
return []
|
||||
|
||||
def test_gline_enforcement():
|
||||
print("\n=== TESTING GLINE ENFORCEMENT ===")
|
||||
|
||||
# Connect admin client
|
||||
admin = connect_client("AdminUser")
|
||||
if not admin:
|
||||
print("Failed to connect admin")
|
||||
return
|
||||
|
||||
read_responses(admin, "Admin", 1)
|
||||
|
||||
# Give admin operator privileges (OPER)
|
||||
admin.send("OPER admin adminpass\r\n".encode())
|
||||
read_responses(admin, "Admin", 1)
|
||||
|
||||
# Connect a test user first
|
||||
print("\n1. Connecting test user before GLINE...")
|
||||
test_user = connect_client("TestUser")
|
||||
if test_user:
|
||||
read_responses(test_user, "TestUser", 1)
|
||||
print(" ✓ TestUser connected successfully")
|
||||
|
||||
# Issue GLINE command
|
||||
print("\n2. Issuing GLINE for TestUser...")
|
||||
admin.send("GLINE TestUser 1h Testing GLINE enforcement\r\n".encode())
|
||||
read_responses(admin, "Admin", 1)
|
||||
|
||||
# Try to send a message as the existing user
|
||||
print("\n3. TestUser trying to send message after GLINE...")
|
||||
if test_user:
|
||||
test_user.send("PRIVMSG #test :This should work, GLINE only affects new connections\r\n".encode())
|
||||
read_responses(test_user, "TestUser", 1)
|
||||
|
||||
# Try to connect a new user with same nick pattern
|
||||
print("\n4. Trying to connect new TestUser after GLINE...")
|
||||
new_test_user = connect_client("TestUser2")
|
||||
if new_test_user:
|
||||
read_responses(new_test_user, "TestUser2", 2)
|
||||
new_test_user.close()
|
||||
|
||||
# Cleanup
|
||||
if test_user:
|
||||
test_user.close()
|
||||
admin.close()
|
||||
|
||||
def test_shun_enforcement():
|
||||
print("\n\n=== TESTING SHUN ENFORCEMENT ===")
|
||||
|
||||
# Connect admin client
|
||||
admin = connect_client("AdminUser")
|
||||
if not admin:
|
||||
print("Failed to connect admin")
|
||||
return
|
||||
|
||||
read_responses(admin, "Admin", 1)
|
||||
|
||||
# Give admin operator privileges
|
||||
admin.send("OPER admin adminpass\r\n".encode())
|
||||
read_responses(admin, "Admin", 1)
|
||||
|
||||
# Connect test user
|
||||
print("\n1. Connecting test user...")
|
||||
test_user = connect_client("ShunUser")
|
||||
if not test_user:
|
||||
print("Failed to connect test user")
|
||||
admin.close()
|
||||
return
|
||||
|
||||
read_responses(test_user, "ShunUser", 1)
|
||||
|
||||
# Test user sends a message before SHUN
|
||||
print("\n2. ShunUser sending message before SHUN...")
|
||||
test_user.send("PRIVMSG #test :This message should work\r\n".encode())
|
||||
read_responses(test_user, "ShunUser", 1)
|
||||
|
||||
# Issue SHUN command
|
||||
print("\n3. Issuing SHUN for ShunUser...")
|
||||
admin.send("SHUN ShunUser 1h Testing SHUN enforcement\r\n".encode())
|
||||
read_responses(admin, "Admin", 1)
|
||||
|
||||
# Test user tries to send message after SHUN
|
||||
print("\n4. ShunUser trying to send message after SHUN (should be ignored)...")
|
||||
test_user.send("PRIVMSG #test :This message should be silently ignored\r\n".encode())
|
||||
read_responses(test_user, "ShunUser", 2)
|
||||
|
||||
# Test user tries other commands
|
||||
print("\n5. ShunUser trying other commands after SHUN...")
|
||||
test_user.send("JOIN #testchan\r\n".encode())
|
||||
read_responses(test_user, "ShunUser", 1)
|
||||
|
||||
test_user.send("NOTICE #test :This notice should also be ignored\r\n".encode())
|
||||
read_responses(test_user, "ShunUser", 1)
|
||||
|
||||
# Cleanup
|
||||
test_user.close()
|
||||
admin.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing TechIRCd Ban Enforcement")
|
||||
print("Make sure TechIRCd server is running on localhost:6667")
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
test_gline_enforcement()
|
||||
test_shun_enforcement()
|
||||
|
||||
print("\n=== TEST COMPLETE ===")
|
||||
23
test_cap_list.sh
Normal file
23
test_cap_list.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script to verify CAP LIST functionality
|
||||
echo "Testing CAP LIST functionality..."
|
||||
|
||||
# Connect to server and test CAP negotiation + LIST
|
||||
{
|
||||
echo "CAP LS"
|
||||
sleep 0.1
|
||||
echo "CAP REQ :away-notify chghost multi-prefix"
|
||||
sleep 0.1
|
||||
echo "CAP LIST"
|
||||
sleep 0.1
|
||||
echo "CAP END"
|
||||
sleep 0.1
|
||||
echo "NICK TestUser"
|
||||
sleep 0.1
|
||||
echo "USER test 0 * :Test User"
|
||||
sleep 0.1
|
||||
echo "CAP LIST"
|
||||
sleep 0.1
|
||||
echo "QUIT :Testing complete"
|
||||
} | telnet localhost 6667
|
||||
56
test_shutdown.sh
Normal file
56
test_shutdown.sh
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TechIRCd Shutdown Test Script
|
||||
echo "=== TechIRCd Shutdown Test ==="
|
||||
echo
|
||||
|
||||
# Start the server in background
|
||||
echo "Starting TechIRCd..."
|
||||
./techircd start &
|
||||
SERVER_PID=$!
|
||||
|
||||
echo "Server started with PID: $SERVER_PID"
|
||||
echo "Waiting 5 seconds for server to initialize..."
|
||||
sleep 5
|
||||
|
||||
# Test connection
|
||||
echo "Testing initial connection..."
|
||||
timeout 5s bash -c '
|
||||
exec 3<>/dev/tcp/localhost/6667
|
||||
echo "NICK TestUser" >&3
|
||||
echo "USER testuser 0 * :Test User" >&3
|
||||
sleep 2
|
||||
echo "PING :test" >&3
|
||||
read -t 3 response <&3
|
||||
echo "Response: $response"
|
||||
exec 3<&-
|
||||
exec 3>&-
|
||||
' && echo "✅ Connection test passed" || echo "❌ Connection test failed"
|
||||
|
||||
echo
|
||||
echo "Testing graceful shutdown (SIGTERM)..."
|
||||
echo "Sending SIGTERM to server..."
|
||||
|
||||
# Send SIGTERM and monitor for graceful shutdown
|
||||
kill -TERM $SERVER_PID
|
||||
|
||||
# Wait up to 15 seconds for graceful shutdown
|
||||
TIMEOUT=15
|
||||
for i in $(seq 1 $TIMEOUT); do
|
||||
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||
echo "✅ Server shut down gracefully after $i seconds"
|
||||
echo
|
||||
echo "=== Shutdown Test Complete ==="
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
echo -n "."
|
||||
done
|
||||
|
||||
echo
|
||||
echo "⚠️ Server did not shut down gracefully within $TIMEOUT seconds"
|
||||
echo "Forcing shutdown..."
|
||||
kill -9 $SERVER_PID 2>/dev/null
|
||||
|
||||
echo
|
||||
echo "=== Shutdown Test Complete ==="
|
||||
138
test_stress.sh
Normal file
138
test_stress.sh
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TechIRCd Stability Stress Test
|
||||
echo "=== TechIRCd Stability Stress Test ==="
|
||||
echo "This test simulates conditions that previously caused freezing"
|
||||
echo
|
||||
|
||||
# Start the server
|
||||
echo "Starting TechIRCd..."
|
||||
./techircd start &
|
||||
SERVER_PID=$!
|
||||
|
||||
echo "Server started with PID: $SERVER_PID"
|
||||
echo "Waiting 3 seconds for server to initialize..."
|
||||
sleep 3
|
||||
|
||||
# Function to create a client connection
|
||||
create_client() {
|
||||
local client_id=$1
|
||||
local duration=$2
|
||||
|
||||
timeout ${duration}s bash -c "
|
||||
exec 3<>/dev/tcp/localhost/6667
|
||||
echo 'NICK TestUser$client_id' >&3
|
||||
echo 'USER testuser$client_id 0 * :Test User $client_id' >&3
|
||||
sleep 2
|
||||
echo 'JOIN #test' >&3
|
||||
|
||||
# Send periodic messages to keep connection alive
|
||||
for i in {1..20}; do
|
||||
echo 'PING :client$client_id' >&3
|
||||
sleep 1
|
||||
echo 'PRIVMSG #test :Message \$i from client $client_id' >&3
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo 'QUIT :Test complete' >&3
|
||||
exec 3<&-
|
||||
exec 3>&-
|
||||
" 2>/dev/null &
|
||||
}
|
||||
|
||||
# Function to create unstable client (connects and disconnects quickly)
|
||||
create_unstable_client() {
|
||||
local client_id=$1
|
||||
|
||||
timeout 5s bash -c "
|
||||
exec 3<>/dev/tcp/localhost/6667
|
||||
echo 'NICK Unstable$client_id' >&3
|
||||
echo 'USER unstable$client_id 0 * :Unstable User $client_id' >&3
|
||||
sleep 1
|
||||
# Abruptly close connection without QUIT
|
||||
exec 3<&-
|
||||
exec 3>&-
|
||||
" 2>/dev/null &
|
||||
}
|
||||
|
||||
echo "Phase 1: Testing multiple stable connections..."
|
||||
# Create 10 stable clients
|
||||
for i in {1..10}; do
|
||||
create_client $i 30
|
||||
echo -n "."
|
||||
sleep 0.5
|
||||
done
|
||||
echo " (10 stable clients created)"
|
||||
|
||||
echo "Phase 2: Testing unstable connections (simulating network issues)..."
|
||||
# Create 20 unstable clients that disconnect abruptly
|
||||
for i in {1..20}; do
|
||||
create_unstable_client $i
|
||||
echo -n "."
|
||||
sleep 0.2
|
||||
done
|
||||
echo " (20 unstable clients created)"
|
||||
|
||||
echo "Phase 3: Testing rapid connection attempts..."
|
||||
# Create many short-lived connections
|
||||
for i in {1..30}; do
|
||||
timeout 2s bash -c "
|
||||
exec 3<>/dev/tcp/localhost/6667
|
||||
echo 'NICK Rapid$i' >&3
|
||||
exec 3<&-
|
||||
exec 3>&-
|
||||
" 2>/dev/null &
|
||||
sleep 0.1
|
||||
done
|
||||
echo "30 rapid connections created"
|
||||
|
||||
echo
|
||||
echo "Monitoring server health for 45 seconds..."
|
||||
echo "Press Ctrl+C to interrupt test early"
|
||||
|
||||
# Monitor server for 45 seconds
|
||||
for i in {1..45}; do
|
||||
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||
echo "❌ Server died during stress test!"
|
||||
echo "=== Stress Test Failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test server responsiveness every 10 seconds
|
||||
if [ $((i % 10)) -eq 0 ]; then
|
||||
echo "Testing server responsiveness at ${i}s..."
|
||||
timeout 3s bash -c '
|
||||
exec 3<>/dev/tcp/localhost/6667
|
||||
echo "NICK HealthCheck$RANDOM" >&3
|
||||
echo "USER healthcheck 0 * :Health Check" >&3
|
||||
sleep 1
|
||||
echo "QUIT :Health check complete" >&3
|
||||
exec 3<&-
|
||||
exec 3>&-
|
||||
' 2>/dev/null && echo "✅ Server responsive" || echo "⚠️ Server response slow/failed"
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo
|
||||
echo "✅ Server survived 45-second stress test!"
|
||||
|
||||
echo "Testing graceful shutdown after stress..."
|
||||
kill -TERM $SERVER_PID
|
||||
|
||||
# Wait for shutdown
|
||||
TIMEOUT=15
|
||||
for i in $(seq 1 $TIMEOUT); do
|
||||
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||
echo "✅ Server shut down gracefully after stress test"
|
||||
echo "=== Stress Test Passed ==="
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "⚠️ Server did not shut down gracefully, forcing..."
|
||||
kill -9 $SERVER_PID 2>/dev/null
|
||||
echo "=== Stress Test Completed with Warning ==="
|
||||
272
tools/build.go
Normal file
272
tools/build.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
binaryName = "techircd"
|
||||
version = "1.0.0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
buildFlag = flag.Bool("build", false, "Build the binary")
|
||||
runFlag = flag.Bool("run", false, "Build and run the server")
|
||||
testFlag = flag.Bool("test", false, "Run all tests")
|
||||
cleanFlag = flag.Bool("clean", false, "Clean build artifacts")
|
||||
fmtFlag = flag.Bool("fmt", false, "Format Go code")
|
||||
lintFlag = flag.Bool("lint", false, "Run linters")
|
||||
buildAllFlag = flag.Bool("build-all", false, "Build for multiple platforms")
|
||||
releaseFlag = flag.Bool("release", false, "Create optimized release build")
|
||||
helpFlag = flag.Bool("help", false, "Show help message")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *helpFlag || flag.NFlag() == 0 {
|
||||
showHelp()
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case *buildFlag:
|
||||
build()
|
||||
case *runFlag:
|
||||
build()
|
||||
run()
|
||||
case *testFlag:
|
||||
test()
|
||||
case *cleanFlag:
|
||||
clean()
|
||||
case *fmtFlag:
|
||||
format()
|
||||
case *lintFlag:
|
||||
lint()
|
||||
case *buildAllFlag:
|
||||
buildAll()
|
||||
case *releaseFlag:
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Println("TechIRCd Build Tool")
|
||||
fmt.Println("")
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" go run build.go [options]")
|
||||
fmt.Println("")
|
||||
fmt.Println("Options:")
|
||||
fmt.Println(" -build Build the binary")
|
||||
fmt.Println(" -run Build and run the server")
|
||||
fmt.Println(" -test Run all tests")
|
||||
fmt.Println(" -clean Clean build artifacts")
|
||||
fmt.Println(" -fmt Format Go code")
|
||||
fmt.Println(" -lint Run linters")
|
||||
fmt.Println(" -build-all Build for multiple platforms")
|
||||
fmt.Println(" -release Create optimized release build")
|
||||
fmt.Println(" -help Show this help message")
|
||||
}
|
||||
|
||||
func build() {
|
||||
fmt.Println("Building TechIRCd...")
|
||||
|
||||
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
|
||||
var versionStr string
|
||||
if err != nil {
|
||||
versionStr = version
|
||||
} else {
|
||||
versionStr = strings.TrimSpace(string(gitVersion))
|
||||
}
|
||||
|
||||
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
|
||||
|
||||
cmd := exec.Command("go", "build", ldflags, "-o", binaryName, ".")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Build failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Build completed successfully!")
|
||||
}
|
||||
|
||||
func run() {
|
||||
fmt.Println("Starting TechIRCd...")
|
||||
|
||||
cmd := exec.Command("./" + binaryName)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Run failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func test() {
|
||||
fmt.Println("Running tests...")
|
||||
|
||||
cmd := exec.Command("go", "test", "-v", "-race", "./...")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Tests failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("All tests passed!")
|
||||
}
|
||||
|
||||
func clean() {
|
||||
fmt.Println("Cleaning build artifacts...")
|
||||
|
||||
// Remove binary files
|
||||
patterns := []string{
|
||||
binaryName + "*",
|
||||
"coverage.out",
|
||||
"coverage.html",
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
if err := os.Remove(match); err != nil {
|
||||
fmt.Printf("Failed to remove %s: %v\n", match, err)
|
||||
} else {
|
||||
fmt.Printf("Removed %s\n", match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Clean completed!")
|
||||
}
|
||||
|
||||
func format() {
|
||||
fmt.Println("Formatting Go code...")
|
||||
|
||||
cmd := exec.Command("go", "fmt", "./...")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Format failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Try to run goimports if available
|
||||
if _, err := exec.LookPath("goimports"); err == nil {
|
||||
fmt.Println("Running goimports...")
|
||||
cmd := exec.Command("goimports", "-w", "-local", "github.com/ComputerTech312/TechIRCd", ".")
|
||||
cmd.Run() // Don't fail if this doesn't work
|
||||
}
|
||||
|
||||
fmt.Println("Format completed!")
|
||||
}
|
||||
|
||||
func lint() {
|
||||
fmt.Println("Running linters...")
|
||||
|
||||
if _, err := exec.LookPath("golangci-lint"); err != nil {
|
||||
fmt.Println("golangci-lint not found, skipping...")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("golangci-lint", "run")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Linting found issues: %v\n", err)
|
||||
// Don't exit on lint errors, just report them
|
||||
} else {
|
||||
fmt.Println("No linting issues found!")
|
||||
}
|
||||
}
|
||||
|
||||
func buildAll() {
|
||||
fmt.Println("Building for multiple platforms...")
|
||||
|
||||
platforms := []struct {
|
||||
goos string
|
||||
goarch string
|
||||
ext string
|
||||
}{
|
||||
{"linux", "amd64", ""},
|
||||
{"windows", "amd64", ".exe"},
|
||||
{"darwin", "amd64", ""},
|
||||
{"darwin", "arm64", ""},
|
||||
}
|
||||
|
||||
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
|
||||
var versionStr string
|
||||
if err != nil {
|
||||
versionStr = version
|
||||
} else {
|
||||
versionStr = strings.TrimSpace(string(gitVersion))
|
||||
}
|
||||
|
||||
for _, platform := range platforms {
|
||||
outputName := fmt.Sprintf("%s-%s-%s%s", binaryName, platform.goos, platform.goarch, platform.ext)
|
||||
fmt.Printf("Building %s...\n", outputName)
|
||||
|
||||
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
|
||||
|
||||
cmd := exec.Command("go", "build", ldflags, "-o", outputName, ".")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS="+platform.goos,
|
||||
"GOARCH="+platform.goarch,
|
||||
)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Failed to build %s: %v\n", outputName, err)
|
||||
} else {
|
||||
fmt.Printf("Built %s successfully!\n", outputName)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Cross-platform build completed!")
|
||||
}
|
||||
|
||||
func release() {
|
||||
fmt.Println("Creating optimized release build...")
|
||||
|
||||
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
|
||||
var versionStr string
|
||||
if err != nil {
|
||||
versionStr = version
|
||||
} else {
|
||||
versionStr = strings.TrimSpace(string(gitVersion))
|
||||
}
|
||||
|
||||
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
|
||||
|
||||
cmd := exec.Command("go", "build", ldflags, "-a", "-installsuffix", "cgo", "-o", binaryName, ".")
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Release build failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get file info to show size
|
||||
if info, err := os.Stat(binaryName); err == nil {
|
||||
fmt.Printf("Release build completed! Binary size: %.2f MB\n", float64(info.Size())/1024/1024)
|
||||
} else {
|
||||
fmt.Println("Release build completed!")
|
||||
}
|
||||
}
|
||||
525
tools/go_stress_test.go
Normal file
525
tools/go_stress_test.go
Normal file
@@ -0,0 +1,525 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StressConfig defines the configuration for stress testing
|
||||
type StressConfig struct {
|
||||
Server struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
} `json:"server"`
|
||||
|
||||
Test struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
} `json:"test"`
|
||||
|
||||
Behavior struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
} `json:"behavior"`
|
||||
|
||||
Channels []string `json:"channels"`
|
||||
Messages []string `json:"messages"`
|
||||
Commands []string `json:"commands"`
|
||||
}
|
||||
|
||||
// IRCClient represents a single IRC client connection
|
||||
type IRCClient struct {
|
||||
ID int
|
||||
Nick string
|
||||
Conn net.Conn
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Channels []string
|
||||
Active bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// StressTest manages the entire stress testing operation
|
||||
type StressTest struct {
|
||||
Config *StressConfig
|
||||
Clients []*IRCClient
|
||||
Stats *TestStats
|
||||
quit chan bool
|
||||
}
|
||||
|
||||
// TestStats tracks testing statistics
|
||||
type TestStats struct {
|
||||
ConnectedClients int
|
||||
MessagesSent int
|
||||
CommandsSent int
|
||||
ChannelsJoined int
|
||||
Errors int
|
||||
StartTime time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from JSON file
|
||||
func LoadConfig(filename string) (*StressConfig, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config := &StressConfig{}
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(config); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config: %v", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
func CreateDefaultConfig(filename string) error {
|
||||
config := &StressConfig{
|
||||
Server: struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "localhost",
|
||||
Port: 6667,
|
||||
},
|
||||
Test: struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
}{
|
||||
MaxClients: 500,
|
||||
ConnectDelay: 50,
|
||||
ActionInterval: 1000,
|
||||
TestDuration: 300,
|
||||
RandomSeed: 42,
|
||||
},
|
||||
Behavior: struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
}{
|
||||
JoinChannels: true,
|
||||
SendMessages: true,
|
||||
ChangeNicks: true,
|
||||
UseNewCommands: true,
|
||||
RandomQuit: false,
|
||||
MessageRate: 0.3,
|
||||
ChannelJoinRate: 0.2,
|
||||
CommandRate: 0.1,
|
||||
},
|
||||
Channels: []string{
|
||||
"#test", "#stress", "#general", "#random", "#chaos",
|
||||
"#lobby", "#gaming", "#tech", "#chat", "#help",
|
||||
},
|
||||
Messages: []string{
|
||||
"Hello everyone!",
|
||||
"This is a stress test message",
|
||||
"How is everyone doing?",
|
||||
"Testing the server stability",
|
||||
"Random message from client",
|
||||
"IRC is awesome!",
|
||||
"TechIRCd rocks!",
|
||||
"Can you see this message?",
|
||||
"Stress testing in progress",
|
||||
"Everything working fine here",
|
||||
},
|
||||
Commands: []string{
|
||||
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
|
||||
"WHO #test", "WHOIS testuser", "LIST",
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// NewStressTest creates a new stress test instance
|
||||
func NewStressTest(config *StressConfig) *StressTest {
|
||||
return &StressTest{
|
||||
Config: config,
|
||||
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
|
||||
Stats: &TestStats{
|
||||
StartTime: time.Now(),
|
||||
},
|
||||
quit: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes connection to IRC server
|
||||
func (c *IRCClient) Connect(host string, port int) error {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
|
||||
c.Conn = conn
|
||||
c.Reader = bufio.NewReader(conn)
|
||||
c.Writer = bufio.NewWriter(conn)
|
||||
c.Active = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register performs IRC client registration
|
||||
func (c *IRCClient) Register() error {
|
||||
commands := []string{
|
||||
fmt.Sprintf("NICK %s", c.Nick),
|
||||
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
if err := c.SendCommand(cmd); err != nil {
|
||||
return fmt.Errorf("registration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendCommand sends a command to the IRC server
|
||||
func (c *IRCClient) SendCommand(command string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.Active || c.Writer == nil {
|
||||
return fmt.Errorf("client not active")
|
||||
}
|
||||
|
||||
_, err := c.Writer.WriteString(command + "\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Writer.Flush()
|
||||
}
|
||||
|
||||
// ReadMessages continuously reads messages from server
|
||||
func (c *IRCClient) ReadMessages(stats *TestStats) {
|
||||
defer func() {
|
||||
c.Active = false
|
||||
if c.Conn != nil {
|
||||
c.Conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for c.Active {
|
||||
if c.Reader == nil {
|
||||
break
|
||||
}
|
||||
|
||||
line, err := c.Reader.ReadString('\n')
|
||||
if err != nil {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
// Handle PING responses
|
||||
if len(line) > 4 && line[:4] == "PING" {
|
||||
pong := "PONG" + line[4:]
|
||||
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PerformRandomAction performs a random IRC action
|
||||
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
|
||||
if !c.Active {
|
||||
return
|
||||
}
|
||||
|
||||
action := rand.Float64()
|
||||
|
||||
switch {
|
||||
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
|
||||
c.SendRandomMessage(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
|
||||
c.JoinRandomChannel(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
|
||||
c.SendRandomCommand(config, stats)
|
||||
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
|
||||
c.ChangeNick(stats)
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomMessage sends a random message to a random channel
|
||||
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
|
||||
if len(c.Channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
channel := c.Channels[rand.Intn(len(c.Channels))]
|
||||
message := config.Messages[rand.Intn(len(config.Messages))]
|
||||
|
||||
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.MessagesSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// JoinRandomChannel joins a random channel
|
||||
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
|
||||
channel := config.Channels[rand.Intn(len(config.Channels))]
|
||||
|
||||
// Check if already in channel
|
||||
for _, ch := range c.Channels {
|
||||
if ch == channel {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("JOIN %s", channel)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Channels = append(c.Channels, channel)
|
||||
stats.mu.Lock()
|
||||
stats.ChannelsJoined++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomCommand sends a random IRC command
|
||||
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
|
||||
command := config.Commands[rand.Intn(len(config.Commands))]
|
||||
|
||||
if err := c.SendCommand(command); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.CommandsSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeNick changes the client's nickname
|
||||
func (c *IRCClient) ChangeNick(stats *TestStats) {
|
||||
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
|
||||
cmd := fmt.Sprintf("NICK %s", newNick)
|
||||
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Nick = newNick
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateClient creates and connects a new IRC client
|
||||
func (st *StressTest) CreateClient(id int) error {
|
||||
client := &IRCClient{
|
||||
ID: id,
|
||||
Nick: fmt.Sprintf("StressUser%d", id),
|
||||
Channels: make([]string, 0),
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
|
||||
return fmt.Errorf("client %d connection failed: %v", id, err)
|
||||
}
|
||||
|
||||
// Register with IRC server
|
||||
if err := client.Register(); err != nil {
|
||||
client.Conn.Close()
|
||||
return fmt.Errorf("client %d registration failed: %v", id, err)
|
||||
}
|
||||
|
||||
st.Clients = append(st.Clients, client)
|
||||
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.ConnectedClients++
|
||||
st.Stats.mu.Unlock()
|
||||
|
||||
// Start message reader goroutine
|
||||
go client.ReadMessages(st.Stats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunStressTest executes the complete stress test
|
||||
func (st *StressTest) RunStressTest() error {
|
||||
log.Printf("Starting stress test with %d clients", st.Config.Test.MaxClients)
|
||||
|
||||
// Set random seed
|
||||
rand.Seed(int64(st.Config.Test.RandomSeed))
|
||||
|
||||
// Connect clients gradually
|
||||
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
|
||||
for i := 0; i < st.Config.Test.MaxClients; i++ {
|
||||
if err := st.CreateClient(i); err != nil {
|
||||
log.Printf("Failed to create client %d: %v", i, err)
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.Errors++
|
||||
st.Stats.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if i%10 == 0 {
|
||||
log.Printf("Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
|
||||
}
|
||||
|
||||
time.Sleep(connectDelay)
|
||||
}
|
||||
|
||||
log.Printf("All clients connected. Starting activity simulation...")
|
||||
|
||||
// Start activity simulation
|
||||
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
|
||||
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
|
||||
|
||||
actionTicker := time.NewTicker(actionInterval)
|
||||
defer actionTicker.Stop()
|
||||
|
||||
statsTicker := time.NewTicker(10 * time.Second)
|
||||
defer statsTicker.Stop()
|
||||
|
||||
testTimer := time.NewTimer(testDuration)
|
||||
defer testTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-actionTicker.C:
|
||||
// Perform random actions for random clients
|
||||
numActions := rand.Intn(len(st.Clients)/4) + 1
|
||||
for i := 0; i < numActions; i++ {
|
||||
if len(st.Clients) > 0 {
|
||||
clientIndex := rand.Intn(len(st.Clients))
|
||||
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
case <-statsTicker.C:
|
||||
st.PrintStats()
|
||||
|
||||
case <-testTimer.C:
|
||||
log.Println("Test duration completed. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
|
||||
case <-st.quit:
|
||||
log.Println("Test interrupted. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintStats prints current test statistics
|
||||
func (st *StressTest) PrintStats() {
|
||||
st.Stats.mu.Lock()
|
||||
defer st.Stats.mu.Unlock()
|
||||
|
||||
elapsed := time.Since(st.Stats.StartTime)
|
||||
|
||||
log.Printf("=== STRESS TEST STATS ===")
|
||||
log.Printf("Runtime: %v", elapsed.Round(time.Second))
|
||||
log.Printf("Connected Clients: %d", st.Stats.ConnectedClients)
|
||||
log.Printf("Messages Sent: %d", st.Stats.MessagesSent)
|
||||
log.Printf("Commands Sent: %d", st.Stats.CommandsSent)
|
||||
log.Printf("Channels Joined: %d", st.Stats.ChannelsJoined)
|
||||
log.Printf("Errors: %d", st.Stats.Errors)
|
||||
|
||||
if elapsed.Seconds() > 0 {
|
||||
log.Printf("Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
|
||||
log.Printf("Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
|
||||
}
|
||||
log.Printf("========================")
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down all clients
|
||||
func (st *StressTest) Shutdown() {
|
||||
log.Println("Shutting down all clients...")
|
||||
|
||||
for i, client := range st.Clients {
|
||||
if client.Active {
|
||||
client.SendCommand("QUIT :Stress test completed")
|
||||
client.Active = false
|
||||
if client.Conn != nil {
|
||||
client.Conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if i%50 == 0 {
|
||||
log.Printf("Disconnected %d/%d clients", i+1, len(st.Clients))
|
||||
}
|
||||
}
|
||||
|
||||
st.PrintStats()
|
||||
log.Println("Stress test completed!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := "stress_test_config.json"
|
||||
|
||||
// Check if config file exists, create default if not
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("Config file %s not found. Creating default...", configFile)
|
||||
if err := CreateDefaultConfig(configFile); err != nil {
|
||||
log.Fatalf("Failed to create default config: %v", err)
|
||||
}
|
||||
log.Printf("Default config created at %s. Please review and modify as needed.", configFile)
|
||||
log.Println("Run the command again to start the stress test.")
|
||||
return
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
|
||||
|
||||
// Create and run stress test
|
||||
stressTest := NewStressTest(config)
|
||||
|
||||
if err := stressTest.RunStressTest(); err != nil {
|
||||
log.Fatalf("Stress test failed: %v", err)
|
||||
}
|
||||
}
|
||||
19
tools/run_stress_test.sh
Normal file
19
tools/run_stress_test.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build and run the Go IRC Stress Tester
|
||||
|
||||
echo "Building Go IRC Stress Tester..."
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Build the stress tester
|
||||
go build -o irc_stress_test go_stress_test.go
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Build successful!"
|
||||
echo "Running stress test..."
|
||||
echo "========================"
|
||||
./irc_stress_test
|
||||
else
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
528
tools/stress_test.go
Normal file
528
tools/stress_test.go
Normal file
@@ -0,0 +1,528 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StressConfig defines the configuration for stress testing
|
||||
type StressConfig struct {
|
||||
Server struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
} `json:"server"`
|
||||
|
||||
Test struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
} `json:"test"`
|
||||
|
||||
Behavior struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
} `json:"behavior"`
|
||||
|
||||
Channels []string `json:"channels"`
|
||||
|
||||
Messages []string `json:"messages"`
|
||||
|
||||
Commands []string `json:"commands"`
|
||||
}
|
||||
|
||||
// IRCClient represents a single IRC client connection
|
||||
type IRCClient struct {
|
||||
ID int
|
||||
Nick string
|
||||
Conn net.Conn
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Channels []string
|
||||
Active bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// StressTest manages the entire stress testing operation
|
||||
type StressTest struct {
|
||||
Config *StressConfig
|
||||
Clients []*IRCClient
|
||||
Stats *TestStats
|
||||
wg sync.WaitGroup
|
||||
quit chan bool
|
||||
}
|
||||
|
||||
// TestStats tracks testing statistics
|
||||
type TestStats struct {
|
||||
ConnectedClients int
|
||||
MessagesSent int
|
||||
CommandsSent int
|
||||
ChannelsJoined int
|
||||
Errors int
|
||||
StartTime time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from JSON file
|
||||
func LoadConfig(filename string) (*StressConfig, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config := &StressConfig{}
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(config); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config: %v", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
func CreateDefaultConfig(filename string) error {
|
||||
config := &StressConfig{
|
||||
Server: struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "localhost",
|
||||
Port: 6667,
|
||||
},
|
||||
Test: struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
}{
|
||||
MaxClients: 500,
|
||||
ConnectDelay: 50,
|
||||
ActionInterval: 1000,
|
||||
TestDuration: 300,
|
||||
RandomSeed: 42,
|
||||
},
|
||||
Behavior: struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
}{
|
||||
JoinChannels: true,
|
||||
SendMessages: true,
|
||||
ChangeNicks: true,
|
||||
UseNewCommands: true,
|
||||
RandomQuit: false,
|
||||
MessageRate: 0.3,
|
||||
ChannelJoinRate: 0.2,
|
||||
CommandRate: 0.1,
|
||||
},
|
||||
Channels: []string{
|
||||
"#test", "#stress", "#general", "#random", "#chaos",
|
||||
"#lobby", "#gaming", "#tech", "#chat", "#help",
|
||||
},
|
||||
Messages: []string{
|
||||
"Hello everyone!",
|
||||
"This is a stress test message",
|
||||
"How is everyone doing?",
|
||||
"Testing the server stability",
|
||||
"Random message from client",
|
||||
"IRC is awesome!",
|
||||
"TechIRCd rocks!",
|
||||
"Can you see this message?",
|
||||
"Stress testing in progress",
|
||||
"Everything working fine here",
|
||||
},
|
||||
Commands: []string{
|
||||
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
|
||||
"WHO #test", "WHOIS testuser", "LIST",
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// NewStressTest creates a new stress test instance
|
||||
func NewStressTest(config *StressConfig) *StressTest {
|
||||
return &StressTest{
|
||||
Config: config,
|
||||
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
|
||||
Stats: &TestStats{
|
||||
StartTime: time.Now(),
|
||||
},
|
||||
quit: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes connection to IRC server
|
||||
func (c *IRCClient) Connect(host string, port int) error {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
|
||||
c.Conn = conn
|
||||
c.Reader = bufio.NewReader(conn)
|
||||
c.Writer = bufio.NewWriter(conn)
|
||||
c.Active = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register performs IRC client registration
|
||||
func (c *IRCClient) Register() error {
|
||||
commands := []string{
|
||||
fmt.Sprintf("NICK %s", c.Nick),
|
||||
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
if err := c.SendCommand(cmd); err != nil {
|
||||
return fmt.Errorf("registration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendCommand sends a command to the IRC server
|
||||
func (c *IRCClient) SendCommand(command string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.Active || c.Writer == nil {
|
||||
return fmt.Errorf("client not active")
|
||||
}
|
||||
|
||||
_, err := c.Writer.WriteString(command + "\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Writer.Flush()
|
||||
}
|
||||
|
||||
// ReadMessages continuously reads messages from server
|
||||
func (c *IRCClient) ReadMessages(stats *TestStats) {
|
||||
defer func() {
|
||||
c.Active = false
|
||||
if c.Conn != nil {
|
||||
c.Conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for c.Active {
|
||||
if c.Reader == nil {
|
||||
break
|
||||
}
|
||||
|
||||
line, err := c.Reader.ReadString('\n')
|
||||
if err != nil {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
// Handle PING responses
|
||||
if len(line) > 4 && line[:4] == "PING" {
|
||||
pong := "PONG" + line[4:]
|
||||
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PerformRandomAction performs a random IRC action
|
||||
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
|
||||
if !c.Active {
|
||||
return
|
||||
}
|
||||
|
||||
action := rand.Float64()
|
||||
|
||||
switch {
|
||||
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
|
||||
c.SendRandomMessage(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
|
||||
c.JoinRandomChannel(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
|
||||
c.SendRandomCommand(config, stats)
|
||||
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
|
||||
c.ChangeNick(stats)
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomMessage sends a random message to a random channel
|
||||
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
|
||||
if len(c.Channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
channel := c.Channels[rand.Intn(len(c.Channels))]
|
||||
message := config.Messages[rand.Intn(len(config.Messages))]
|
||||
|
||||
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.MessagesSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// JoinRandomChannel joins a random channel
|
||||
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
|
||||
channel := config.Channels[rand.Intn(len(config.Channels))]
|
||||
|
||||
// Check if already in channel
|
||||
for _, ch := range c.Channels {
|
||||
if ch == channel {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("JOIN %s", channel)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Channels = append(c.Channels, channel)
|
||||
stats.mu.Lock()
|
||||
stats.ChannelsJoined++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomCommand sends a random IRC command
|
||||
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
|
||||
command := config.Commands[rand.Intn(len(config.Commands))]
|
||||
|
||||
if err := c.SendCommand(command); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.CommandsSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeNick changes the client's nickname
|
||||
func (c *IRCClient) ChangeNick(stats *TestStats) {
|
||||
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
|
||||
cmd := fmt.Sprintf("NICK %s", newNick)
|
||||
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Nick = newNick
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateClient creates and connects a new IRC client
|
||||
func (st *StressTest) CreateClient(id int) error {
|
||||
client := &IRCClient{
|
||||
ID: id,
|
||||
Nick: fmt.Sprintf("StressUser%d", id),
|
||||
Channels: make([]string, 0),
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
|
||||
return fmt.Errorf("client %d connection failed: %v", id, err)
|
||||
}
|
||||
|
||||
// Register with IRC server
|
||||
if err := client.Register(); err != nil {
|
||||
client.Conn.Close()
|
||||
return fmt.Errorf("client %d registration failed: %v", id, err)
|
||||
}
|
||||
|
||||
st.Clients = append(st.Clients, client)
|
||||
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.ConnectedClients++
|
||||
st.Stats.mu.Unlock()
|
||||
|
||||
// Start message reader goroutine
|
||||
go client.ReadMessages(st.Stats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunStressTest executes the complete stress test
|
||||
func (st *StressTest) RunStressTest() error {
|
||||
log.Printf("Starting stress test with %d clients", st.Config.Test.MaxClients)
|
||||
|
||||
// Set random seed
|
||||
rand.Seed(int64(st.Config.Test.RandomSeed))
|
||||
|
||||
// Connect clients gradually
|
||||
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
|
||||
for i := 0; i < st.Config.Test.MaxClients; i++ {
|
||||
if err := st.CreateClient(i); err != nil {
|
||||
log.Printf("Failed to create client %d: %v", i, err)
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.Errors++
|
||||
st.Stats.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if i%10 == 0 {
|
||||
log.Printf("Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
|
||||
}
|
||||
|
||||
time.Sleep(connectDelay)
|
||||
}
|
||||
|
||||
log.Printf("All clients connected. Starting activity simulation...")
|
||||
|
||||
// Start activity simulation
|
||||
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
|
||||
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
|
||||
|
||||
actionTicker := time.NewTicker(actionInterval)
|
||||
defer actionTicker.Stop()
|
||||
|
||||
statsTicker := time.NewTicker(10 * time.Second)
|
||||
defer statsTicker.Stop()
|
||||
|
||||
testTimer := time.NewTimer(testDuration)
|
||||
defer testTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-actionTicker.C:
|
||||
// Perform random actions for random clients
|
||||
numActions := rand.Intn(len(st.Clients)/4) + 1
|
||||
for i := 0; i < numActions; i++ {
|
||||
if len(st.Clients) > 0 {
|
||||
clientIndex := rand.Intn(len(st.Clients))
|
||||
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
case <-statsTicker.C:
|
||||
st.PrintStats()
|
||||
|
||||
case <-testTimer.C:
|
||||
log.Println("Test duration completed. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
|
||||
case <-st.quit:
|
||||
log.Println("Test interrupted. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintStats prints current test statistics
|
||||
func (st *StressTest) PrintStats() {
|
||||
st.Stats.mu.Lock()
|
||||
defer st.Stats.mu.Unlock()
|
||||
|
||||
elapsed := time.Since(st.Stats.StartTime)
|
||||
|
||||
log.Printf("=== STRESS TEST STATS ===")
|
||||
log.Printf("Runtime: %v", elapsed.Round(time.Second))
|
||||
log.Printf("Connected Clients: %d", st.Stats.ConnectedClients)
|
||||
log.Printf("Messages Sent: %d", st.Stats.MessagesSent)
|
||||
log.Printf("Commands Sent: %d", st.Stats.CommandsSent)
|
||||
log.Printf("Channels Joined: %d", st.Stats.ChannelsJoined)
|
||||
log.Printf("Errors: %d", st.Stats.Errors)
|
||||
|
||||
if elapsed.Seconds() > 0 {
|
||||
log.Printf("Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
|
||||
log.Printf("Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
|
||||
}
|
||||
log.Printf("========================")
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down all clients
|
||||
func (st *StressTest) Shutdown() {
|
||||
log.Println("Shutting down all clients...")
|
||||
|
||||
for i, client := range st.Clients {
|
||||
if client.Active {
|
||||
client.SendCommand("QUIT :Stress test completed")
|
||||
client.Active = false
|
||||
if client.Conn != nil {
|
||||
client.Conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if i%50 == 0 {
|
||||
log.Printf("Disconnected %d/%d clients", i+1, len(st.Clients))
|
||||
}
|
||||
}
|
||||
|
||||
st.PrintStats()
|
||||
log.Println("Stress test completed!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := "stress_test_config.json"
|
||||
|
||||
// Check if config file exists, create default if not
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("Config file %s not found. Creating default...", configFile)
|
||||
if err := CreateDefaultConfig(configFile); err != nil {
|
||||
log.Fatalf("Failed to create default config: %v", err)
|
||||
}
|
||||
log.Printf("Default config created at %s. Please review and modify as needed.", configFile)
|
||||
log.Println("Run the command again to start the stress test.")
|
||||
return
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
|
||||
|
||||
// Create and run stress test
|
||||
stressTest := NewStressTest(config)
|
||||
|
||||
if err := stressTest.RunStressTest(); err != nil {
|
||||
log.Fatalf("Stress test failed: %v", err)
|
||||
}
|
||||
}
|
||||
464
tools/stress_test.py
Normal file
464
tools/stress_test.py
Normal file
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TechIRCd Advanced Stress Tester
|
||||
===============================
|
||||
Comprehensive IRC server stress testing tool with configurable behavior.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
import json
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION - Edit these values to customize your stress test
|
||||
# =============================================================================
|
||||
|
||||
CONFIG = {
|
||||
# Server connection settings
|
||||
"SERVER": {
|
||||
"host": "localhost",
|
||||
"port": 6667,
|
||||
},
|
||||
|
||||
# Test parameters
|
||||
"TEST": {
|
||||
"max_clients": 300, # Number of concurrent clients
|
||||
"connect_delay": 0.05, # Seconds between connections
|
||||
"test_duration": 300, # Test duration in seconds
|
||||
"action_interval": 1.0, # Seconds between random actions
|
||||
"stats_interval": 10, # Seconds between stats reports
|
||||
},
|
||||
|
||||
# Client behavior probabilities (0.0 = never, 1.0 = always)
|
||||
"BEHAVIOR": {
|
||||
"join_channels": True,
|
||||
"send_messages": True,
|
||||
"use_new_commands": True,
|
||||
"change_nicks": True,
|
||||
"random_quit": False,
|
||||
|
||||
# Action rates (probability per action cycle)
|
||||
"message_rate": 0.4, # Chance to send a message
|
||||
"channel_join_rate": 0.2, # Chance to join a channel
|
||||
"command_rate": 0.15, # Chance to send a command
|
||||
"nick_change_rate": 0.05, # Chance to change nick
|
||||
"quit_rate": 0.01, # Chance to quit and reconnect
|
||||
},
|
||||
|
||||
# IRC channels to use
|
||||
"CHANNELS": [
|
||||
"#test", "#stress", "#general", "#random", "#chaos",
|
||||
"#lobby", "#gaming", "#tech", "#chat", "#help",
|
||||
"#dev", "#admin", "#support", "#lounge", "#public"
|
||||
],
|
||||
|
||||
# Random messages to send
|
||||
"MESSAGES": [
|
||||
"Hello everyone!",
|
||||
"This is a stress test message",
|
||||
"How is everyone doing today?",
|
||||
"Testing server stability under load",
|
||||
"Random message from stress test client",
|
||||
"IRC is still the best chat protocol!",
|
||||
"TechIRCd is handling this load well",
|
||||
"Can you see this message?",
|
||||
"Stress testing in progress...",
|
||||
"Everything working fine here",
|
||||
"Anyone else here for the stress test?",
|
||||
"Server performance looking good!",
|
||||
"Testing new IRC commands",
|
||||
"This channel is quite active",
|
||||
"Load testing is important for stability"
|
||||
],
|
||||
|
||||
# IRC commands to test (including new ones!)
|
||||
"COMMANDS": [
|
||||
"MOTD",
|
||||
"RULES",
|
||||
"MAP",
|
||||
"TIME",
|
||||
"VERSION",
|
||||
"LUSERS",
|
||||
"WHO #test",
|
||||
"WHOIS testuser",
|
||||
"LIST",
|
||||
"ADMIN",
|
||||
"INFO",
|
||||
"KNOCK #test",
|
||||
"SETNAME :New real name from stress test"
|
||||
],
|
||||
|
||||
# Logging configuration
|
||||
"LOGGING": {
|
||||
"level": "INFO", # DEBUG, INFO, WARNING, ERROR
|
||||
"show_irc_traffic": False, # Set to True to see all IRC messages
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# STRESS TESTING CODE - Don't modify unless you know what you're doing
|
||||
# =============================================================================
|
||||
|
||||
class StressTestStats:
|
||||
"""Tracks statistics during stress testing"""
|
||||
|
||||
def __init__(self):
|
||||
self.start_time = time.time()
|
||||
self.connected_clients = 0
|
||||
self.messages_sent = 0
|
||||
self.commands_sent = 0
|
||||
self.channels_joined = 0
|
||||
self.nick_changes = 0
|
||||
self.errors = 0
|
||||
self.reconnections = 0
|
||||
|
||||
def runtime(self) -> float:
|
||||
return time.time() - self.start_time
|
||||
|
||||
def print_stats(self):
|
||||
runtime = self.runtime()
|
||||
print("\n" + "="*50)
|
||||
print("STRESS TEST STATISTICS")
|
||||
print("="*50)
|
||||
print(f"Runtime: {runtime:.1f}s")
|
||||
print(f"Connected Clients: {self.connected_clients}")
|
||||
print(f"Messages Sent: {self.messages_sent}")
|
||||
print(f"Commands Sent: {self.commands_sent}")
|
||||
print(f"Channels Joined: {self.channels_joined}")
|
||||
print(f"Nick Changes: {self.nick_changes}")
|
||||
print(f"Reconnections: {self.reconnections}")
|
||||
print(f"Errors: {self.errors}")
|
||||
|
||||
if runtime > 0:
|
||||
print(f"Messages/sec: {self.messages_sent/runtime:.2f}")
|
||||
print(f"Commands/sec: {self.commands_sent/runtime:.2f}")
|
||||
print(f"Actions/sec: {(self.messages_sent + self.commands_sent)/runtime:.2f}")
|
||||
|
||||
print("="*50)
|
||||
|
||||
class IRCStressClient:
|
||||
"""Individual IRC client for stress testing"""
|
||||
|
||||
def __init__(self, client_id: int, stats: StressTestStats):
|
||||
self.client_id = client_id
|
||||
self.nick = f"StressUser{client_id}"
|
||||
self.user = f"stress{client_id}"
|
||||
self.realname = f"Stress Test Client {client_id}"
|
||||
self.channels = []
|
||||
self.stats = stats
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.connected = False
|
||||
self.registered = False
|
||||
self.running = True
|
||||
|
||||
async def connect(self, host: str, port: int) -> bool:
|
||||
"""Connect to IRC server"""
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection(host, port)
|
||||
self.connected = True
|
||||
logging.debug(f"Client {self.client_id} connected to {host}:{port}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} connection failed: {e}")
|
||||
self.stats.errors += 1
|
||||
return False
|
||||
|
||||
async def register(self) -> bool:
|
||||
"""Register with IRC server"""
|
||||
try:
|
||||
await self.send_command(f"NICK {self.nick}")
|
||||
await self.send_command(f"USER {self.user} 0 * :{self.realname}")
|
||||
self.registered = True
|
||||
self.stats.connected_clients += 1
|
||||
logging.debug(f"Client {self.client_id} registered as {self.nick}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} registration failed: {e}")
|
||||
self.stats.errors += 1
|
||||
return False
|
||||
|
||||
async def send_command(self, command: str):
|
||||
"""Send IRC command to server"""
|
||||
if not self.connected or not self.writer:
|
||||
return
|
||||
|
||||
try:
|
||||
message = f"{command}\r\n"
|
||||
self.writer.write(message.encode())
|
||||
await self.writer.drain()
|
||||
|
||||
if CONFIG["LOGGING"]["show_irc_traffic"]:
|
||||
logging.debug(f"Client {self.client_id} >>> {command}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} send error: {e}")
|
||||
self.stats.errors += 1
|
||||
|
||||
async def read_messages(self):
|
||||
"""Read and handle messages from server"""
|
||||
while self.running and self.connected:
|
||||
try:
|
||||
if not self.reader:
|
||||
break
|
||||
|
||||
line = await self.reader.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
message = line.decode().strip()
|
||||
if not message:
|
||||
continue
|
||||
|
||||
if CONFIG["LOGGING"]["show_irc_traffic"]:
|
||||
logging.debug(f"Client {self.client_id} <<< {message}")
|
||||
|
||||
# Handle PING
|
||||
if message.startswith("PING"):
|
||||
pong = message.replace("PING", "PONG", 1)
|
||||
await self.send_command(pong)
|
||||
|
||||
# Handle other server messages if needed
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} read error: {e}")
|
||||
self.stats.errors += 1
|
||||
break
|
||||
|
||||
async def join_random_channel(self):
|
||||
"""Join a random channel"""
|
||||
if not CONFIG["BEHAVIOR"]["join_channels"]:
|
||||
return
|
||||
|
||||
channel = random.choice(CONFIG["CHANNELS"])
|
||||
if channel not in self.channels:
|
||||
await self.send_command(f"JOIN {channel}")
|
||||
self.channels.append(channel)
|
||||
self.stats.channels_joined += 1
|
||||
logging.debug(f"Client {self.client_id} joined {channel}")
|
||||
|
||||
async def send_random_message(self):
|
||||
"""Send a random message to a random channel"""
|
||||
if not CONFIG["BEHAVIOR"]["send_messages"] or not self.channels:
|
||||
return
|
||||
|
||||
channel = random.choice(self.channels)
|
||||
message = random.choice(CONFIG["MESSAGES"])
|
||||
await self.send_command(f"PRIVMSG {channel} :{message}")
|
||||
self.stats.messages_sent += 1
|
||||
logging.debug(f"Client {self.client_id} sent message to {channel}")
|
||||
|
||||
async def send_random_command(self):
|
||||
"""Send a random IRC command"""
|
||||
if not CONFIG["BEHAVIOR"]["use_new_commands"]:
|
||||
return
|
||||
|
||||
command = random.choice(CONFIG["COMMANDS"])
|
||||
await self.send_command(command)
|
||||
self.stats.commands_sent += 1
|
||||
logging.debug(f"Client {self.client_id} sent command: {command}")
|
||||
|
||||
async def change_nick(self):
|
||||
"""Change nickname"""
|
||||
if not CONFIG["BEHAVIOR"]["change_nicks"]:
|
||||
return
|
||||
|
||||
new_nick = f"User{self.client_id}_{random.randint(1, 9999)}"
|
||||
await self.send_command(f"NICK {new_nick}")
|
||||
self.nick = new_nick
|
||||
self.stats.nick_changes += 1
|
||||
logging.debug(f"Client {self.client_id} changed nick to {new_nick}")
|
||||
|
||||
async def random_quit_reconnect(self):
|
||||
"""Randomly quit and reconnect"""
|
||||
if not CONFIG["BEHAVIOR"]["random_quit"]:
|
||||
return
|
||||
|
||||
await self.send_command("QUIT :Reconnecting...")
|
||||
await self.disconnect()
|
||||
|
||||
# Wait a moment then reconnect
|
||||
await asyncio.sleep(random.uniform(1, 3))
|
||||
|
||||
if await self.connect(CONFIG["SERVER"]["host"], CONFIG["SERVER"]["port"]):
|
||||
await self.register()
|
||||
self.stats.reconnections += 1
|
||||
logging.debug(f"Client {self.client_id} reconnected")
|
||||
|
||||
async def perform_random_action(self):
|
||||
"""Perform a random IRC action"""
|
||||
if not self.registered:
|
||||
return
|
||||
|
||||
action = random.random()
|
||||
|
||||
if action < CONFIG["BEHAVIOR"]["message_rate"]:
|
||||
await self.send_random_message()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"]:
|
||||
await self.join_random_channel()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"]:
|
||||
await self.send_random_command()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"] + CONFIG["BEHAVIOR"]["nick_change_rate"]:
|
||||
await self.change_nick()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"] + CONFIG["BEHAVIOR"]["nick_change_rate"] + CONFIG["BEHAVIOR"]["quit_rate"]:
|
||||
await self.random_quit_reconnect()
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from server"""
|
||||
self.running = False
|
||||
self.connected = False
|
||||
|
||||
if self.writer:
|
||||
try:
|
||||
await self.send_command("QUIT :Stress test completed")
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.registered:
|
||||
self.stats.connected_clients -= 1
|
||||
self.registered = False
|
||||
|
||||
class IRCStressTester:
|
||||
"""Main stress testing coordinator"""
|
||||
|
||||
def __init__(self):
|
||||
self.clients: List[IRCStressClient] = []
|
||||
self.stats = StressTestStats()
|
||||
self.running = False
|
||||
|
||||
# Setup logging
|
||||
log_level = getattr(logging, CONFIG["LOGGING"]["level"])
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
async def create_client(self, client_id: int) -> Optional[IRCStressClient]:
|
||||
"""Create and connect a new client"""
|
||||
client = IRCStressClient(client_id, self.stats)
|
||||
|
||||
if await client.connect(CONFIG["SERVER"]["host"], CONFIG["SERVER"]["port"]):
|
||||
if await client.register():
|
||||
# Start message reader
|
||||
asyncio.create_task(client.read_messages())
|
||||
return client
|
||||
|
||||
return None
|
||||
|
||||
async def connect_all_clients(self):
|
||||
"""Connect all clients with delay"""
|
||||
print(f"Connecting {CONFIG['TEST']['max_clients']} clients...")
|
||||
|
||||
for i in range(CONFIG["TEST"]["max_clients"]):
|
||||
client = await self.create_client(i)
|
||||
if client:
|
||||
self.clients.append(client)
|
||||
|
||||
# Join initial channel
|
||||
if CONFIG["BEHAVIOR"]["join_channels"]:
|
||||
await client.join_random_channel()
|
||||
|
||||
# Progress reporting
|
||||
if (i + 1) % 10 == 0:
|
||||
print(f"Connected {i + 1}/{CONFIG['TEST']['max_clients']} clients")
|
||||
|
||||
# Delay between connections
|
||||
if CONFIG["TEST"]["connect_delay"] > 0:
|
||||
await asyncio.sleep(CONFIG["TEST"]["connect_delay"])
|
||||
|
||||
print(f"All {len(self.clients)} clients connected!")
|
||||
|
||||
async def run_activity_simulation(self):
|
||||
"""Run the main activity simulation"""
|
||||
print("Starting activity simulation...")
|
||||
|
||||
start_time = time.time()
|
||||
last_stats = time.time()
|
||||
|
||||
while self.running and (time.time() - start_time) < CONFIG["TEST"]["test_duration"]:
|
||||
# Perform random actions
|
||||
active_clients = [c for c in self.clients if c.registered]
|
||||
if active_clients:
|
||||
# Select random clients to perform actions
|
||||
num_actions = random.randint(1, len(active_clients) // 4 + 1)
|
||||
selected_clients = random.sample(active_clients, min(num_actions, len(active_clients)))
|
||||
|
||||
# Perform actions concurrently
|
||||
tasks = [client.perform_random_action() for client in selected_clients]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Print stats periodically
|
||||
if time.time() - last_stats >= CONFIG["TEST"]["stats_interval"]:
|
||||
self.stats.print_stats()
|
||||
last_stats = time.time()
|
||||
|
||||
# Wait before next action cycle
|
||||
await asyncio.sleep(CONFIG["TEST"]["action_interval"])
|
||||
|
||||
async def shutdown_all_clients(self):
|
||||
"""Gracefully disconnect all clients"""
|
||||
print("Shutting down all clients...")
|
||||
|
||||
disconnect_tasks = []
|
||||
for client in self.clients:
|
||||
disconnect_tasks.append(client.disconnect())
|
||||
|
||||
# Wait for all disconnections
|
||||
await asyncio.gather(*disconnect_tasks, return_exceptions=True)
|
||||
|
||||
print("All clients disconnected.")
|
||||
|
||||
async def run_stress_test(self):
|
||||
"""Run the complete stress test"""
|
||||
print("="*60)
|
||||
print("TECHIRCD ADVANCED STRESS TESTER")
|
||||
print("="*60)
|
||||
print(f"Target: {CONFIG['SERVER']['host']}:{CONFIG['SERVER']['port']}")
|
||||
print(f"Clients: {CONFIG['TEST']['max_clients']}")
|
||||
print(f"Duration: {CONFIG['TEST']['test_duration']}s")
|
||||
print(f"Channels: {len(CONFIG['CHANNELS'])}")
|
||||
print(f"Commands: {len(CONFIG['COMMANDS'])}")
|
||||
print("="*60)
|
||||
|
||||
self.running = True
|
||||
|
||||
try:
|
||||
# Connect all clients
|
||||
await self.connect_all_clients()
|
||||
|
||||
# Run activity simulation
|
||||
await self.run_activity_simulation()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nTest interrupted by user!")
|
||||
except Exception as e:
|
||||
print(f"\nTest failed with error: {e}")
|
||||
finally:
|
||||
self.running = False
|
||||
await self.shutdown_all_clients()
|
||||
|
||||
# Final stats
|
||||
print("\nFINAL RESULTS:")
|
||||
self.stats.print_stats()
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
stress_tester = IRCStressTester()
|
||||
await stress_tester.run_stress_test()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting TechIRCd Advanced Stress Tester...")
|
||||
print("Press Ctrl+C to stop the test early.\n")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nTest terminated by user.")
|
||||
except Exception as e:
|
||||
print(f"\nFatal error: {e}")
|
||||
533
tools/stress_tester.go
Normal file
533
tools/stress_tester.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package main
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StressConfig defines the configuration for stress testing
|
||||
type StressConfig struct {
|
||||
Server struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
} `json:"server"`
|
||||
|
||||
Test struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
} `json:"test"`
|
||||
|
||||
Behavior struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
} `json:"behavior"`
|
||||
|
||||
Channels []string `json:"channels"`
|
||||
Messages []string `json:"messages"`
|
||||
Commands []string `json:"commands"`
|
||||
}
|
||||
|
||||
// IRCClient represents a single IRC client connection
|
||||
type IRCClient struct {
|
||||
ID int
|
||||
Nick string
|
||||
Conn net.Conn
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Channels []string
|
||||
Active bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// StressTest manages the entire stress testing operation
|
||||
type StressTest struct {
|
||||
Config *StressConfig
|
||||
Clients []*IRCClient
|
||||
Stats *TestStats
|
||||
quit chan bool
|
||||
}
|
||||
|
||||
// TestStats tracks testing statistics
|
||||
type TestStats struct {
|
||||
ConnectedClients int
|
||||
MessagesSent int
|
||||
CommandsSent int
|
||||
ChannelsJoined int
|
||||
Errors int
|
||||
StartTime time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from JSON file
|
||||
func LoadConfig(filename string) (*StressConfig, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config := &StressConfig{}
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(config); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config: %v", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
func CreateDefaultConfig(filename string) error {
|
||||
config := &StressConfig{
|
||||
Server: struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "localhost",
|
||||
Port: 6667,
|
||||
},
|
||||
Test: struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
}{
|
||||
MaxClients: 300,
|
||||
ConnectDelay: 100,
|
||||
ActionInterval: 2000,
|
||||
TestDuration: 180,
|
||||
RandomSeed: 42,
|
||||
},
|
||||
Behavior: struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
}{
|
||||
JoinChannels: true,
|
||||
SendMessages: true,
|
||||
ChangeNicks: true,
|
||||
UseNewCommands: true,
|
||||
RandomQuit: false,
|
||||
MessageRate: 0.4,
|
||||
ChannelJoinRate: 0.3,
|
||||
CommandRate: 0.2,
|
||||
},
|
||||
Channels: []string{
|
||||
"#test", "#stress", "#general", "#random", "#chaos",
|
||||
"#lobby", "#gaming", "#tech", "#chat", "#help",
|
||||
},
|
||||
Messages: []string{
|
||||
"Hello everyone!",
|
||||
"This is a stress test message",
|
||||
"How is everyone doing?",
|
||||
"Testing the server stability",
|
||||
"Random message from client",
|
||||
"IRC is awesome!",
|
||||
"TechIRCd rocks!",
|
||||
"Can you see this message?",
|
||||
"Stress testing in progress",
|
||||
"Everything working fine here",
|
||||
"Testing new IRC commands",
|
||||
"Server performance looks good",
|
||||
},
|
||||
Commands: []string{
|
||||
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
|
||||
"WHO #test", "WHOIS testuser", "LIST", "WHOWAS olduser",
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// NewStressTest creates a new stress test instance
|
||||
func NewStressTest(config *StressConfig) *StressTest {
|
||||
return &StressTest{
|
||||
Config: config,
|
||||
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
|
||||
Stats: &TestStats{
|
||||
StartTime: time.Now(),
|
||||
},
|
||||
quit: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes connection to IRC server
|
||||
func (c *IRCClient) Connect(host string, port int) error {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
|
||||
c.Conn = conn
|
||||
c.Reader = bufio.NewReader(conn)
|
||||
c.Writer = bufio.NewWriter(conn)
|
||||
c.Active = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register performs IRC client registration
|
||||
func (c *IRCClient) Register() error {
|
||||
commands := []string{
|
||||
fmt.Sprintf("NICK %s", c.Nick),
|
||||
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
if err := c.SendCommand(cmd); err != nil {
|
||||
return fmt.Errorf("registration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendCommand sends a command to the IRC server
|
||||
func (c *IRCClient) SendCommand(command string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.Active || c.Writer == nil {
|
||||
return fmt.Errorf("client not active")
|
||||
}
|
||||
|
||||
_, err := c.Writer.WriteString(command + "\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Writer.Flush()
|
||||
}
|
||||
|
||||
// ReadMessages continuously reads messages from server
|
||||
func (c *IRCClient) ReadMessages(stats *TestStats) {
|
||||
defer func() {
|
||||
c.Active = false
|
||||
if c.Conn != nil {
|
||||
c.Conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for c.Active {
|
||||
if c.Reader == nil {
|
||||
break
|
||||
}
|
||||
|
||||
line, err := c.Reader.ReadString('\n')
|
||||
if err != nil {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
// Handle PING responses
|
||||
if len(line) > 4 && line[:4] == "PING" {
|
||||
pong := "PONG" + line[4:]
|
||||
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PerformRandomAction performs a random IRC action
|
||||
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
|
||||
if !c.Active {
|
||||
return
|
||||
}
|
||||
|
||||
action := rand.Float64()
|
||||
|
||||
switch {
|
||||
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
|
||||
c.SendRandomMessage(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
|
||||
c.JoinRandomChannel(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
|
||||
c.SendRandomCommand(config, stats)
|
||||
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
|
||||
c.ChangeNick(stats)
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomMessage sends a random message to a random channel
|
||||
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
|
||||
if len(c.Channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
channel := c.Channels[rand.Intn(len(c.Channels))]
|
||||
message := config.Messages[rand.Intn(len(config.Messages))]
|
||||
|
||||
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.MessagesSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// JoinRandomChannel joins a random channel
|
||||
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
|
||||
channel := config.Channels[rand.Intn(len(config.Channels))]
|
||||
|
||||
// Check if already in channel
|
||||
for _, ch := range c.Channels {
|
||||
if ch == channel {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("JOIN %s", channel)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Channels = append(c.Channels, channel)
|
||||
stats.mu.Lock()
|
||||
stats.ChannelsJoined++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomCommand sends a random IRC command
|
||||
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
|
||||
command := config.Commands[rand.Intn(len(config.Commands))]
|
||||
|
||||
if err := c.SendCommand(command); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.CommandsSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeNick changes the client's nickname
|
||||
func (c *IRCClient) ChangeNick(stats *TestStats) {
|
||||
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
|
||||
cmd := fmt.Sprintf("NICK %s", newNick)
|
||||
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Nick = newNick
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateClient creates and connects a new IRC client
|
||||
func (st *StressTest) CreateClient(id int) error {
|
||||
client := &IRCClient{
|
||||
ID: id,
|
||||
Nick: fmt.Sprintf("StressUser%d", id),
|
||||
Channels: make([]string, 0),
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
|
||||
return fmt.Errorf("client %d connection failed: %v", id, err)
|
||||
}
|
||||
|
||||
// Register with IRC server
|
||||
if err := client.Register(); err != nil {
|
||||
client.Conn.Close()
|
||||
return fmt.Errorf("client %d registration failed: %v", id, err)
|
||||
}
|
||||
|
||||
st.Clients = append(st.Clients, client)
|
||||
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.ConnectedClients++
|
||||
st.Stats.mu.Unlock()
|
||||
|
||||
// Start message reader goroutine
|
||||
go client.ReadMessages(st.Stats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunStressTest executes the complete stress test
|
||||
func (st *StressTest) RunStressTest() error {
|
||||
log.Printf("🚀 Starting TechIRCd stress test with %d clients", st.Config.Test.MaxClients)
|
||||
log.Printf("📡 Target server: %s:%d", st.Config.Server.Host, st.Config.Server.Port)
|
||||
log.Printf("⏱️ Test duration: %d seconds", st.Config.Test.TestDuration)
|
||||
|
||||
// Set random seed
|
||||
rand.Seed(int64(st.Config.Test.RandomSeed))
|
||||
|
||||
// Connect clients gradually
|
||||
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
|
||||
for i := 0; i < st.Config.Test.MaxClients; i++ {
|
||||
if err := st.CreateClient(i); err != nil {
|
||||
log.Printf("❌ Failed to create client %d: %v", i, err)
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.Errors++
|
||||
st.Stats.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if i%25 == 0 {
|
||||
log.Printf("🔗 Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
|
||||
}
|
||||
|
||||
time.Sleep(connectDelay)
|
||||
}
|
||||
|
||||
log.Printf("✅ All clients connected! Starting chaos simulation...")
|
||||
|
||||
// Start activity simulation
|
||||
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
|
||||
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
|
||||
|
||||
actionTicker := time.NewTicker(actionInterval)
|
||||
defer actionTicker.Stop()
|
||||
|
||||
statsTicker := time.NewTicker(15 * time.Second)
|
||||
defer statsTicker.Stop()
|
||||
|
||||
testTimer := time.NewTimer(testDuration)
|
||||
defer testTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-actionTicker.C:
|
||||
// Perform random actions for random clients
|
||||
numActions := rand.Intn(len(st.Clients)/3) + 1
|
||||
for i := 0; i < numActions; i++ {
|
||||
if len(st.Clients) > 0 {
|
||||
clientIndex := rand.Intn(len(st.Clients))
|
||||
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
case <-statsTicker.C:
|
||||
st.PrintStats()
|
||||
|
||||
case <-testTimer.C:
|
||||
log.Println("⏰ Test duration completed. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
|
||||
case <-st.quit:
|
||||
log.Println("🛑 Test interrupted. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintStats prints current test statistics
|
||||
func (st *StressTest) PrintStats() {
|
||||
st.Stats.mu.Lock()
|
||||
defer st.Stats.mu.Unlock()
|
||||
|
||||
elapsed := time.Since(st.Stats.StartTime)
|
||||
|
||||
log.Printf("📊 === STRESS TEST STATS ===")
|
||||
log.Printf("⏱️ Runtime: %v", elapsed.Round(time.Second))
|
||||
log.Printf("👥 Connected Clients: %d", st.Stats.ConnectedClients)
|
||||
log.Printf("💬 Messages Sent: %d", st.Stats.MessagesSent)
|
||||
log.Printf("⚡ Commands Sent: %d", st.Stats.CommandsSent)
|
||||
log.Printf("🏠 Channels Joined: %d", st.Stats.ChannelsJoined)
|
||||
log.Printf("❌ Errors: %d", st.Stats.Errors)
|
||||
|
||||
if elapsed.Seconds() > 0 {
|
||||
log.Printf("📈 Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
|
||||
log.Printf("📈 Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
|
||||
}
|
||||
log.Printf("============================")
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down all clients
|
||||
func (st *StressTest) Shutdown() {
|
||||
log.Println("🔌 Shutting down all clients...")
|
||||
|
||||
for i, client := range st.Clients {
|
||||
if client.Active {
|
||||
client.SendCommand("QUIT :Stress test completed")
|
||||
client.Active = false
|
||||
if client.Conn != nil {
|
||||
client.Conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if i%50 == 0 && i > 0 {
|
||||
log.Printf("🔌 Disconnected %d/%d clients", i+1, len(st.Clients))
|
||||
}
|
||||
}
|
||||
|
||||
st.PrintStats()
|
||||
log.Println("🎉 Stress test completed successfully!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := "stress_test_config.json"
|
||||
|
||||
log.Println("🎯 TechIRCd Go Stress Tester v1.0")
|
||||
log.Println("==================================")
|
||||
|
||||
// Check if config file exists, create default if not
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("📝 Config file %s not found. Creating default...", configFile)
|
||||
if err := CreateDefaultConfig(configFile); err != nil {
|
||||
log.Fatalf("❌ Failed to create default config: %v", err)
|
||||
}
|
||||
log.Printf("✅ Default config created at %s", configFile)
|
||||
log.Println("📖 Please review and modify the config as needed, then run again.")
|
||||
return
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("📋 Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
|
||||
|
||||
// Create and run stress test
|
||||
stressTest := NewStressTest(config)
|
||||
|
||||
if err := stressTest.RunStressTest(); err != nil {
|
||||
log.Fatalf("❌ Stress test failed: %v", err)
|
||||
}
|
||||
}
|
||||
134
validation.go
Normal file
134
validation.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidateConfig performs comprehensive validation of the server configuration
|
||||
func (c *Config) Validate() error {
|
||||
// Validate server settings
|
||||
if c.Server.Name == "" {
|
||||
return fmt.Errorf("server name cannot be empty")
|
||||
}
|
||||
|
||||
if c.Server.Network == "" {
|
||||
return fmt.Errorf("network name cannot be empty")
|
||||
}
|
||||
|
||||
if c.Server.Listen.Host == "" {
|
||||
c.Server.Listen.Host = "localhost" // Default fallback
|
||||
}
|
||||
|
||||
if c.Server.Listen.Port <= 0 || c.Server.Listen.Port > 65535 {
|
||||
return fmt.Errorf("invalid port number: %d", c.Server.Listen.Port)
|
||||
}
|
||||
|
||||
if c.Server.Listen.EnableSSL && (c.Server.Listen.SSLPort <= 0 || c.Server.Listen.SSLPort > 65535) {
|
||||
return fmt.Errorf("invalid SSL port number: %d", c.Server.Listen.SSLPort)
|
||||
}
|
||||
|
||||
// Validate limits
|
||||
if c.Limits.MaxClients <= 0 {
|
||||
c.Limits.MaxClients = 1000 // Default
|
||||
}
|
||||
|
||||
if c.Limits.MaxChannels <= 0 {
|
||||
c.Limits.MaxChannels = 100 // Default
|
||||
}
|
||||
|
||||
if c.Limits.MaxNickLength <= 0 || c.Limits.MaxNickLength > 50 {
|
||||
c.Limits.MaxNickLength = 30 // Default
|
||||
}
|
||||
|
||||
if c.Limits.PingTimeout <= 0 {
|
||||
c.Limits.PingTimeout = 300 // Default 5 minutes
|
||||
}
|
||||
|
||||
if c.Limits.FloodLines <= 0 {
|
||||
c.Limits.FloodLines = 10 // Default
|
||||
}
|
||||
|
||||
if c.Limits.FloodSeconds <= 0 {
|
||||
c.Limits.FloodSeconds = 60 // Default
|
||||
}
|
||||
|
||||
// Validate channels
|
||||
for _, channelName := range c.Channels.AutoJoin {
|
||||
if !isChannelName(channelName) {
|
||||
return fmt.Errorf("invalid channel name in auto_join: %s", channelName)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate default modes
|
||||
validChannelModes := "mntisp"
|
||||
for _, mode := range c.Channels.DefaultModes {
|
||||
if mode != '+' && !strings.ContainsRune(validChannelModes, mode) {
|
||||
return fmt.Errorf("invalid default channel mode: %c", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate founder mode
|
||||
validFounderModes := []string{"q", "a", "o", "h", "v"}
|
||||
foundValidMode := false
|
||||
for _, validMode := range validFounderModes {
|
||||
if c.Channels.FounderMode == validMode {
|
||||
foundValidMode = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundValidMode {
|
||||
return fmt.Errorf("invalid founder_mode '%s', must be one of: q (owner), a (admin), o (operator), h (halfop), v (voice)", c.Channels.FounderMode)
|
||||
}
|
||||
|
||||
// Validate operators
|
||||
for i, oper := range c.Opers {
|
||||
if oper.Name == "" {
|
||||
return fmt.Errorf("operator %d: name cannot be empty", i)
|
||||
}
|
||||
if oper.Password == "" {
|
||||
return fmt.Errorf("operator %s: password cannot be empty", oper.Name)
|
||||
}
|
||||
if oper.Host == "" {
|
||||
return fmt.Errorf("operator %s: host cannot be empty", oper.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SanitizeConfig applies safe defaults and sanitizes configuration values
|
||||
func (c *Config) SanitizeConfig() {
|
||||
// Ensure reasonable limits
|
||||
if c.Limits.MaxClients > 10000 {
|
||||
c.Limits.MaxClients = 10000
|
||||
}
|
||||
|
||||
if c.Limits.MaxChannels > 1000 {
|
||||
c.Limits.MaxChannels = 1000
|
||||
}
|
||||
|
||||
// Set default founder mode if empty or invalid
|
||||
if c.Channels.FounderMode == "" {
|
||||
c.Channels.FounderMode = "o" // Default to operator
|
||||
}
|
||||
|
||||
// Ensure reasonable string lengths
|
||||
if c.Limits.MaxTopicLength > 2048 {
|
||||
c.Limits.MaxTopicLength = 2048
|
||||
}
|
||||
|
||||
if c.Limits.MaxKickLength > 2048 {
|
||||
c.Limits.MaxKickLength = 2048
|
||||
}
|
||||
|
||||
// Ensure MOTD isn't too long
|
||||
if len(c.MOTD) > 50 {
|
||||
c.MOTD = c.MOTD[:50]
|
||||
}
|
||||
|
||||
// Sanitize channel modes
|
||||
if c.Channels.DefaultModes == "" {
|
||||
c.Channels.DefaultModes = "+nt"
|
||||
}
|
||||
}
|
||||
127
web_interface.go
Normal file
127
web_interface.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Web administration interface
|
||||
type WebInterface struct {
|
||||
Enable bool `json:"enable"`
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
SSL struct {
|
||||
Enable bool `json:"enable"`
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
} `json:"ssl"`
|
||||
Authentication struct {
|
||||
Method string `json:"method"` // basic, oauth, jwt
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
} `json:"authentication"`
|
||||
}
|
||||
|
||||
// WebServerStats represents statistics for the web interface
|
||||
type WebServerStats struct {
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
ActiveUsers int `json:"active_users"`
|
||||
ActiveChannels int `json:"active_channels"`
|
||||
LinkedServers int `json:"linked_servers"`
|
||||
MessagesPerSec float64 `json:"messages_per_sec"`
|
||||
MemoryUsage uint64 `json:"memory_usage"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
}
|
||||
|
||||
// WebUserInfo represents user information for the web interface
|
||||
type WebUserInfo struct {
|
||||
Nick string `json:"nick"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Channels int `json:"channels"`
|
||||
IsOper bool `json:"is_oper"`
|
||||
ConnTime time.Time `json:"conn_time"`
|
||||
}
|
||||
|
||||
// REST API endpoints
|
||||
func (w *WebInterface) RegisterRoutes() {
|
||||
http.HandleFunc("/api/v1/server/stats", w.handleServerStats)
|
||||
http.HandleFunc("/api/v1/users", w.handleUsers)
|
||||
http.HandleFunc("/api/v1/channels", w.handleChannels)
|
||||
http.HandleFunc("/api/v1/operators", w.handleOperators)
|
||||
http.HandleFunc("/api/v1/config", w.handleConfig)
|
||||
http.HandleFunc("/api/v1/logs", w.handleLogs)
|
||||
http.HandleFunc("/api/v1/bans", w.handleBans)
|
||||
http.HandleFunc("/api/v1/network", w.handleNetwork)
|
||||
|
||||
// WebSocket for real-time updates
|
||||
http.HandleFunc("/ws/live", w.handleWebSocket)
|
||||
|
||||
// Static files for web interface
|
||||
http.Handle("/", http.FileServer(http.Dir("web/static/")))
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleServerStats(rw http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Get actual server statistics
|
||||
stats := WebServerStats{
|
||||
Uptime: time.Duration(0),
|
||||
ActiveUsers: 0,
|
||||
ActiveChannels: 0,
|
||||
LinkedServers: 0,
|
||||
MessagesPerSec: 0.0,
|
||||
MemoryUsage: 0,
|
||||
CPUUsage: 0.0,
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(rw).Encode(stats)
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleUsers(rw http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
// List all users
|
||||
users := make([]*WebUserInfo, 0)
|
||||
// TODO: Get actual users from server
|
||||
json.NewEncoder(rw).Encode(users)
|
||||
|
||||
case "POST":
|
||||
// Send message to user or perform action
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
|
||||
case "DELETE":
|
||||
// Disconnect user
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleChannels(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleOperators(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleLogs(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleBans(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (w *WebInterface) handleNetwork(rw http.ResponseWriter, r *http.Request) {
|
||||
http.Error(rw, "Not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// Real-time dashboard with WebSocket
|
||||
func (w *WebInterface) handleWebSocket(rw http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Upgrade to WebSocket and send real-time updates
|
||||
http.Error(rw, "WebSocket not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
180
worker_pool.go
Normal file
180
worker_pool.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WorkerPool manages goroutines for handling client connections
|
||||
type WorkerPool struct {
|
||||
workers int
|
||||
jobs chan net.Conn
|
||||
server *Server
|
||||
wg sync.WaitGroup
|
||||
shutdown chan bool
|
||||
stats *PoolStats
|
||||
}
|
||||
|
||||
type PoolStats struct {
|
||||
ActiveWorkers int64
|
||||
ProcessedJobs int64
|
||||
QueuedJobs int64
|
||||
ErrorCount int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWorkerPool creates a new worker pool
|
||||
func NewWorkerPool(workers int, server *Server) *WorkerPool {
|
||||
if workers <= 0 {
|
||||
workers = runtime.NumCPU() * 4 // Default: 4 workers per CPU core
|
||||
}
|
||||
|
||||
return &WorkerPool{
|
||||
workers: workers,
|
||||
jobs: make(chan net.Conn, workers*10), // Buffer for connection queue
|
||||
server: server,
|
||||
shutdown: make(chan bool),
|
||||
stats: &PoolStats{},
|
||||
}
|
||||
}
|
||||
|
||||
// Start initializes and starts the worker pool
|
||||
func (wp *WorkerPool) Start() {
|
||||
log.Printf("Starting worker pool with %d workers", wp.workers)
|
||||
|
||||
for i := 0; i < wp.workers; i++ {
|
||||
wp.wg.Add(1)
|
||||
go wp.worker(i)
|
||||
}
|
||||
|
||||
// Start stats reporter
|
||||
go wp.statsReporter()
|
||||
}
|
||||
|
||||
// worker processes incoming connections
|
||||
func (wp *WorkerPool) worker(id int) {
|
||||
defer wp.wg.Done()
|
||||
|
||||
wp.updateActiveWorkers(1)
|
||||
defer wp.updateActiveWorkers(-1)
|
||||
|
||||
log.Printf("Worker %d started", id)
|
||||
|
||||
for {
|
||||
select {
|
||||
case conn := <-wp.jobs:
|
||||
wp.handleConnection(conn, id)
|
||||
wp.updateProcessedJobs(1)
|
||||
|
||||
case <-wp.shutdown:
|
||||
log.Printf("Worker %d shutting down", id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection processes a single client connection
|
||||
func (wp *WorkerPool) handleConnection(conn net.Conn, workerID int) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Worker %d recovered from panic: %v", workerID, r)
|
||||
wp.updateErrorCount(1)
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Set connection timeouts
|
||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
|
||||
|
||||
client := NewClient(conn, wp.server)
|
||||
wp.server.AddClient(client)
|
||||
|
||||
log.Printf("Worker %d handling client from %s", workerID, conn.RemoteAddr())
|
||||
|
||||
// Handle client in this worker goroutine
|
||||
client.Handle()
|
||||
}
|
||||
|
||||
// Submit adds a connection to the worker pool queue
|
||||
func (wp *WorkerPool) Submit(conn net.Conn) bool {
|
||||
wp.updateQueuedJobs(1)
|
||||
|
||||
select {
|
||||
case wp.jobs <- conn:
|
||||
return true
|
||||
default:
|
||||
// Queue is full
|
||||
wp.updateQueuedJobs(-1)
|
||||
wp.updateErrorCount(1)
|
||||
log.Printf("Worker pool queue full, rejecting connection from %s", conn.RemoteAddr())
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the worker pool
|
||||
func (wp *WorkerPool) Shutdown() {
|
||||
log.Println("Shutting down worker pool...")
|
||||
|
||||
close(wp.shutdown)
|
||||
close(wp.jobs)
|
||||
|
||||
// Wait for all workers to finish
|
||||
wp.wg.Wait()
|
||||
|
||||
log.Println("Worker pool shutdown complete")
|
||||
}
|
||||
|
||||
// Stats update methods
|
||||
func (wp *WorkerPool) updateActiveWorkers(delta int64) {
|
||||
wp.stats.mu.Lock()
|
||||
wp.stats.ActiveWorkers += delta
|
||||
wp.stats.mu.Unlock()
|
||||
}
|
||||
|
||||
func (wp *WorkerPool) updateProcessedJobs(delta int64) {
|
||||
wp.stats.mu.Lock()
|
||||
wp.stats.ProcessedJobs += delta
|
||||
wp.stats.QueuedJobs -= delta
|
||||
wp.stats.mu.Unlock()
|
||||
}
|
||||
|
||||
func (wp *WorkerPool) updateQueuedJobs(delta int64) {
|
||||
wp.stats.mu.Lock()
|
||||
wp.stats.QueuedJobs += delta
|
||||
wp.stats.mu.Unlock()
|
||||
}
|
||||
|
||||
func (wp *WorkerPool) updateErrorCount(delta int64) {
|
||||
wp.stats.mu.Lock()
|
||||
wp.stats.ErrorCount += delta
|
||||
wp.stats.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetStats returns current pool statistics
|
||||
func (wp *WorkerPool) GetStats() PoolStats {
|
||||
wp.stats.mu.RLock()
|
||||
defer wp.stats.mu.RUnlock()
|
||||
return *wp.stats
|
||||
}
|
||||
|
||||
// statsReporter periodically logs worker pool statistics
|
||||
func (wp *WorkerPool) statsReporter() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
stats := wp.GetStats()
|
||||
log.Printf("Worker Pool Stats - Active: %d, Processed: %d, Queued: %d, Errors: %d",
|
||||
stats.ActiveWorkers, stats.ProcessedJobs, stats.QueuedJobs, stats.ErrorCount)
|
||||
|
||||
case <-wp.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user