Fixed the issue where the IsAllowed check was broken, enhanced the robustness of IP verification, and added front-end and back-end token validation
This commit is contained in:
parent
f7b89bbd98
commit
a4e25a0f09
5 changed files with 405 additions and 23 deletions
|
|
@ -139,6 +139,13 @@
|
|||
"webhook_path": "/webhook/wecom-app",
|
||||
"allow_from": [],
|
||||
"reply_timeout": 5
|
||||
},
|
||||
"websocket": {
|
||||
"enabled": false,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8080,
|
||||
"token": "YOUR_TOKEN",
|
||||
"allow_from": []
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -57,6 +58,7 @@ type Channel struct {
|
|||
bus *bus.MessageBus
|
||||
running bool
|
||||
allowList []string
|
||||
token string // Authentication token (optional)
|
||||
server *http.Server
|
||||
upgrader websocket.Upgrader
|
||||
clients sync.Map // map[string]*clientConn
|
||||
|
|
@ -83,10 +85,16 @@ func NewChannel(cfg config.WebSocketConfig, messageBus *bus.MessageBus) (*Channe
|
|||
cfg.Host = "0.0.0.0"
|
||||
}
|
||||
|
||||
// Validate allow_from configuration at startup
|
||||
if err := validateAllowList(cfg.AllowFrom); err != nil {
|
||||
return nil, fmt.Errorf("invalid allow_from configuration: %w", err)
|
||||
}
|
||||
|
||||
return &Channel{
|
||||
config: cfg,
|
||||
bus: messageBus,
|
||||
allowList: cfg.AllowFrom,
|
||||
token: strings.TrimSpace(cfg.Token),
|
||||
running: false,
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
|
|
@ -100,6 +108,55 @@ func NewChannel(cfg config.WebSocketConfig, messageBus *bus.MessageBus) (*Channe
|
|||
}, nil
|
||||
}
|
||||
|
||||
// validateAllowList validates the allow_from configuration at startup
|
||||
// This prevents silent failures during runtime when invalid rules are encountered
|
||||
func validateAllowList(allowList []string) error {
|
||||
if len(allowList) == 0 {
|
||||
// Empty list is valid (allows all)
|
||||
return nil
|
||||
}
|
||||
|
||||
var invalidRules []string
|
||||
validRuleCount := 0
|
||||
|
||||
for _, allowed := range allowList {
|
||||
allowed = strings.TrimSpace(allowed)
|
||||
if allowed == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a CIDR notation
|
||||
if strings.Contains(allowed, "/") {
|
||||
if _, _, err := net.ParseCIDR(allowed); err != nil {
|
||||
invalidRules = append(invalidRules, fmt.Sprintf("%s (CIDR parse error: %v)", allowed, err))
|
||||
continue
|
||||
}
|
||||
validRuleCount++
|
||||
} else {
|
||||
// Validate as plain IP address
|
||||
// Strip zone identifier for validation
|
||||
ipStr := allowed
|
||||
if idx := strings.IndexByte(ipStr, '%'); idx != -1 {
|
||||
ipStr = ipStr[:idx]
|
||||
}
|
||||
if net.ParseIP(ipStr) == nil {
|
||||
invalidRules = append(invalidRules, fmt.Sprintf("%s (invalid IP address)", allowed))
|
||||
continue
|
||||
}
|
||||
validRuleCount++
|
||||
}
|
||||
}
|
||||
|
||||
// If there are invalid rules, return error with details
|
||||
if len(invalidRules) > 0 {
|
||||
return fmt.Errorf("found %d invalid rule(s) in allow_from: %s",
|
||||
len(invalidRules), strings.Join(invalidRules, "; "))
|
||||
}
|
||||
|
||||
// All rules are valid
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the channel name
|
||||
func (c *Channel) Name() string {
|
||||
return "websocket"
|
||||
|
|
@ -112,19 +169,93 @@ func (c *Channel) IsRunning() bool {
|
|||
return c.running
|
||||
}
|
||||
|
||||
// IsAllowed checks if a sender ID is allowed to use this channel
|
||||
func (c *Channel) IsAllowed(senderID string) bool {
|
||||
// IsAllowed checks if a client IP is allowed to use this channel
|
||||
// The clientID should be in the format "ip:port" (from r.RemoteAddr)
|
||||
// Supports:
|
||||
// - Exact IP match: "192.168.1.5"
|
||||
// - CIDR notation: "192.168.1.0/24", "10.0.0.0/8"
|
||||
// - IPv6: "::1", "fe80::/10"
|
||||
func (c *Channel) IsAllowed(clientID string) bool {
|
||||
if len(c.allowList) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Extract IP address from "ip:port" format
|
||||
clientIPStr := c.extractIP(clientID)
|
||||
if clientIPStr == "" {
|
||||
logger.WarnCF("websocket", "Failed to extract IP from client ID", map[string]any{
|
||||
"client_id": clientID,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse client IP (strip zone identifier for IPv6 link-local addresses)
|
||||
clientIPStr = c.stripZone(clientIPStr)
|
||||
clientIP := net.ParseIP(clientIPStr)
|
||||
if clientIP == nil {
|
||||
logger.WarnCF("websocket", "Invalid client IP address", map[string]any{
|
||||
"client_ip": clientIPStr,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Check against allow list
|
||||
for _, allowed := range c.allowList {
|
||||
if strings.EqualFold(allowed, senderID) {
|
||||
return true
|
||||
// Try CIDR notation first
|
||||
if strings.Contains(allowed, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(allowed)
|
||||
if err != nil {
|
||||
// This should never happen as we validate at startup
|
||||
// But keep for safety
|
||||
logger.ErrorCF("websocket", "Invalid CIDR in allow list (should be caught at startup)", map[string]any{
|
||||
"cidr": allowed,
|
||||
"error": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
if ipNet.Contains(clientIP) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Exact IP match (strip zone for comparison)
|
||||
allowedStripped := c.stripZone(allowed)
|
||||
if strings.EqualFold(allowedStripped, clientIPStr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// extractIP extracts the IP address from "ip:port" format
|
||||
// Handles:
|
||||
// - "192.168.1.5:8080" -> "192.168.1.5"
|
||||
// - "[::1]:8080" -> "::1"
|
||||
// - "[::1]" -> "::1" (brackets without port)
|
||||
// - "::1" -> "::1" (no brackets, no port)
|
||||
func (c *Channel) extractIP(addr string) string {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
// Handle IPv6 addresses with brackets but no port: [::1] -> ::1
|
||||
if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
|
||||
return addr[1 : len(addr)-1]
|
||||
}
|
||||
// Otherwise assume it's already just an IP (no port)
|
||||
return addr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// stripZone removes the zone identifier from IPv6 addresses
|
||||
// For example: "fe80::1%eth0" -> "fe80::1"
|
||||
// This is necessary because net.ParseIP doesn't handle zone identifiers
|
||||
func (c *Channel) stripZone(ipStr string) string {
|
||||
if idx := strings.IndexByte(ipStr, '%'); idx != -1 {
|
||||
return ipStr[:idx]
|
||||
}
|
||||
return ipStr
|
||||
}
|
||||
|
||||
// setRunning sets the running state
|
||||
func (c *Channel) setRunning(running bool) {
|
||||
c.clientsMu.Lock()
|
||||
|
|
@ -133,11 +264,8 @@ func (c *Channel) setRunning(running bool) {
|
|||
}
|
||||
|
||||
// HandleMessage processes an incoming message and publishes it to the bus
|
||||
// Note: Authorization is already checked during WebSocket connection establishment
|
||||
func (c *Channel) HandleMessage(senderID, chatID, content string, media []string, metadata map[string]string) {
|
||||
if !c.IsAllowed(senderID) {
|
||||
return
|
||||
}
|
||||
|
||||
// Build session key: channel:chatID
|
||||
sessionKey := fmt.Sprintf("%s:%s", c.Name(), chatID)
|
||||
|
||||
|
|
@ -313,8 +441,74 @@ func min(a, b int) int {
|
|||
return b
|
||||
}
|
||||
|
||||
// validateToken validates the authentication token from the request
|
||||
// Supports multiple token sources:
|
||||
// 1. Query parameter: ?token=xxx
|
||||
// 2. Authorization header: Bearer xxx
|
||||
// 3. Sec-WebSocket-Protocol header: token (WebSocket standard approach)
|
||||
func (c *Channel) validateToken(r *http.Request) bool {
|
||||
if c.token == "" {
|
||||
return true // No token configured, allow
|
||||
}
|
||||
|
||||
// Method 1: Check query parameter
|
||||
if tokenParam := r.URL.Query().Get("token"); tokenParam != "" {
|
||||
return tokenParam == c.token
|
||||
}
|
||||
|
||||
// Method 2: Check Authorization header (Bearer token)
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "" {
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
return token == c.token
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: Check Sec-WebSocket-Protocol header
|
||||
// Client can send: Sec-WebSocket-Protocol: token
|
||||
// This is a standard WebSocket approach for authentication
|
||||
protocol := r.Header.Get("Sec-WebSocket-Protocol")
|
||||
if protocol != "" {
|
||||
// Support both "token" format and "bearer-<token>" format
|
||||
if protocol == c.token {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(protocol, "bearer-") {
|
||||
token := strings.TrimPrefix(protocol, "bearer-")
|
||||
return token == c.token
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// handleWebSocket handles WebSocket connection upgrades
|
||||
func (c *Channel) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Check token authentication if configured
|
||||
if c.token != "" {
|
||||
if !c.validateToken(r) {
|
||||
logger.WarnCF("websocket", "Unauthorized connection attempt - invalid token", map[string]any{
|
||||
"remote_addr": r.RemoteAddr,
|
||||
"user_agent": r.UserAgent(),
|
||||
})
|
||||
http.Error(w, "Unauthorized: invalid or missing token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check IP authorization
|
||||
clientID := r.RemoteAddr
|
||||
if !c.IsAllowed(clientID) {
|
||||
clientIP := c.extractIP(clientID)
|
||||
logger.WarnCF("websocket", "Unauthorized connection attempt - IP not allowed", map[string]any{
|
||||
"client_ip": clientIP,
|
||||
"client_id": clientID,
|
||||
})
|
||||
http.Error(w, "Forbidden: IP not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := c.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
logger.ErrorCF("websocket", "Failed to upgrade connection", map[string]any{
|
||||
|
|
@ -323,13 +517,12 @@ func (c *Channel) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Generate client ID from remote address
|
||||
clientID := r.RemoteAddr
|
||||
client := &clientConn{conn: conn}
|
||||
c.clients.Store(clientID, client)
|
||||
|
||||
logger.InfoCF("websocket", "New client connected", map[string]any{
|
||||
"client_id": clientID,
|
||||
"client_ip": c.extractIP(clientID),
|
||||
})
|
||||
|
||||
// Handle client messages
|
||||
|
|
@ -407,14 +600,6 @@ func (c *Channel) handleClient(clientID string, client *clientConn) {
|
|||
|
||||
// Process chat messages
|
||||
if wsMsg.Type == "chat" {
|
||||
// Check allowlist
|
||||
if !c.IsAllowed(clientID) {
|
||||
logger.WarnCF("websocket", "Unauthorized client", map[string]any{
|
||||
"client_id": clientID,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
logger.DebugCF("websocket", "Received message", map[string]any{
|
||||
"client_id": clientID,
|
||||
"content": wsMsg.Content,
|
||||
|
|
|
|||
|
|
@ -1004,6 +1004,8 @@
|
|||
|
||||
let reconnectCount = 0;
|
||||
let reconnectMessageElement = null;
|
||||
let connectionAttempted = false; // Track if we've tried to connect
|
||||
let hasEverConnected = false; // Track if we've successfully connected before
|
||||
let lastMessageTime = null;
|
||||
|
||||
// Scroll to bottom helper
|
||||
|
|
@ -1072,8 +1074,28 @@
|
|||
}
|
||||
|
||||
function connect() {
|
||||
// Get token from multiple sources (priority order):
|
||||
// 1. URL parameter: ?token=xxx
|
||||
// 2. localStorage
|
||||
// 3. Prompt user
|
||||
let token = new URLSearchParams(window.location.search).get('token');
|
||||
|
||||
if (!token) {
|
||||
token = localStorage.getItem('picoclaw-token');
|
||||
}
|
||||
|
||||
// If still no token and this is first connection attempt, check if token might be required
|
||||
// by attempting connection. Server will return 401 if token is needed.
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = protocol + '//' + window.location.host + '/ws';
|
||||
let wsUrl = protocol + '//' + window.location.host + '/ws';
|
||||
|
||||
// Add token to URL if available
|
||||
if (token) {
|
||||
wsUrl += '?token=' + encodeURIComponent(token);
|
||||
// Save token for future use
|
||||
localStorage.setItem('picoclaw-token', token);
|
||||
}
|
||||
|
||||
// Close existing connection if any
|
||||
if (ws && ws.readyState !== WebSocket.CLOSED) {
|
||||
|
|
@ -1119,9 +1141,109 @@
|
|||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('Disconnected from PicoClaw', event.code, event.reason);
|
||||
console.log('Disconnected from PicoClaw', event.code, event.reason, 'hasEverConnected:', hasEverConnected, 'connectionAttempted:', connectionAttempted, 'hasToken:', !!localStorage.getItem('picoclaw-token'));
|
||||
|
||||
// Increment reconnect count
|
||||
// Scenario 1: First connection attempt failed without token
|
||||
// This likely means server requires token (close code 1006 = abnormal closure from HTTP error)
|
||||
if (!hasEverConnected && connectionAttempted && !localStorage.getItem('picoclaw-token') && event.code === 1006) {
|
||||
const t = currentLang === 'zh'
|
||||
? {
|
||||
authRequired: '连接失败',
|
||||
authPrompt: '服务器可能需要访问令牌(Token)\n\n如果已配置 token,请输入;\n如果未配置 token,请留空并点击「确定」',
|
||||
retry: '重试中...',
|
||||
noAuth: '正在重试连接...'
|
||||
}
|
||||
: {
|
||||
authRequired: 'Connection Failed',
|
||||
authPrompt: 'Server may require access token\n\nIf token is configured, enter it;\nIf not configured, leave empty and click OK',
|
||||
retry: 'Retrying...',
|
||||
noAuth: 'Retrying connection...'
|
||||
};
|
||||
|
||||
statusDiv.innerHTML = '<div class="status-dot"></div><span>' + t.authRequired + '</span>';
|
||||
statusDiv.className = 'status disconnected';
|
||||
messageInput.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
|
||||
// Prompt for token (user can leave empty if server has no token)
|
||||
setTimeout(() => {
|
||||
const token = prompt(t.authPrompt);
|
||||
// User clicked OK (with or without token)
|
||||
if (token !== null) {
|
||||
if (token.trim()) {
|
||||
// User entered a token, save and retry
|
||||
localStorage.setItem('picoclaw-token', token.trim());
|
||||
statusDiv.innerHTML = '<div class="status-dot"></div><span>' + t.retry + '</span>';
|
||||
addMessage('system', t.retry, 'system');
|
||||
setTimeout(connect, 500);
|
||||
} else {
|
||||
// User left empty and clicked OK - server might not require token
|
||||
addMessage('system', t.noAuth, 'system');
|
||||
setTimeout(connect, 2000);
|
||||
}
|
||||
} else {
|
||||
// User clicked Cancel - don't retry
|
||||
const cancelMsg = currentLang === 'zh' ? '已取消连接' : 'Connection cancelled';
|
||||
addMessage('system', cancelMsg, 'system');
|
||||
addRetryButton();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Scenario 2: Connection failed with token present
|
||||
// This means the token is invalid (close code 1006 from HTTP 401)
|
||||
if (event.code === 1006 && localStorage.getItem('picoclaw-token') && !hasEverConnected) {
|
||||
localStorage.removeItem('picoclaw-token');
|
||||
|
||||
const t = currentLang === 'zh'
|
||||
? {
|
||||
authRequired: '认证失败',
|
||||
authPrompt: 'Token 无效或已过期,请重新输入:',
|
||||
authFailed: '认证失败,请检查令牌是否正确',
|
||||
tokenRequired: '需要有效的访问令牌才能连接',
|
||||
retry: '重试中...',
|
||||
cancelled: '已取消连接'
|
||||
}
|
||||
: {
|
||||
authRequired: 'Authentication Failed',
|
||||
authPrompt: 'Token invalid or expired, please enter again:',
|
||||
authFailed: 'Authentication failed, please check your token',
|
||||
tokenRequired: 'Valid access token required to connect',
|
||||
retry: 'Retrying...',
|
||||
cancelled: 'Connection cancelled'
|
||||
};
|
||||
|
||||
statusDiv.innerHTML = '<div class="status-dot"></div><span>' + t.authRequired + '</span>';
|
||||
statusDiv.className = 'status disconnected';
|
||||
messageInput.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
|
||||
// Show auth failure message
|
||||
addMessage('system', t.authFailed, 'system');
|
||||
|
||||
// Prompt for token with retry support
|
||||
setTimeout(() => {
|
||||
const token = prompt(t.authPrompt);
|
||||
if (token && token.trim()) {
|
||||
localStorage.setItem('picoclaw-token', token.trim());
|
||||
statusDiv.innerHTML = '<div class="status-dot"></div><span>' + t.retry + '</span>';
|
||||
addMessage('system', t.retry, 'system');
|
||||
setTimeout(connect, 500);
|
||||
} else {
|
||||
// User cancelled
|
||||
addMessage('system', t.cancelled + ' - ' + t.tokenRequired, 'system');
|
||||
statusDiv.innerHTML = '<div class="status-dot"></div><span>' + t.cancelled + '</span>';
|
||||
messageInput.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
addRetryButton();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Scenario 3: Normal disconnection after successful connection
|
||||
// Increment reconnect count and try to reconnect
|
||||
reconnectCount++;
|
||||
|
||||
const t = currentLang === 'zh'
|
||||
|
|
@ -1151,7 +1273,9 @@
|
|||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
console.error('WebSocket error:', error, 'hasEverConnected:', hasEverConnected, 'hasToken:', !!localStorage.getItem('picoclaw-token'));
|
||||
// Mark that connection has been attempted
|
||||
connectionAttempted = true;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1219,6 +1343,70 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Add a retry button for reconnection with token
|
||||
function addRetryButton() {
|
||||
const t = currentLang === 'zh'
|
||||
? { retryButton: '点击重新连接', clickToRetry: '点击下方按钮重新输入 Token 并连接' }
|
||||
: { retryButton: 'Retry Connection', clickToRetry: 'Click button below to re-enter token and connect' };
|
||||
|
||||
// Create a message with a retry button
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message system';
|
||||
messageDiv.dataset.timestamp = Date.now();
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.style.cursor = 'pointer';
|
||||
contentDiv.style.userSelect = 'none';
|
||||
|
||||
const rawHtml = marked.parse(
|
||||
t.clickToRetry + '\n\n**[' + t.retryButton + ']**'
|
||||
);
|
||||
const cleanHtml = DOMPurify.sanitize(rawHtml);
|
||||
contentDiv.innerHTML = cleanHtml;
|
||||
|
||||
messageDiv.appendChild(contentDiv);
|
||||
messagesDiv.insertBefore(messageDiv, typingIndicator);
|
||||
|
||||
// Make it clickable
|
||||
contentDiv.addEventListener('click', () => {
|
||||
// Remove the retry button message
|
||||
messageDiv.remove();
|
||||
// Trigger reconnection with token prompt
|
||||
promptForToken();
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// Prompt user for token and attempt reconnection
|
||||
function promptForToken() {
|
||||
const t = currentLang === 'zh'
|
||||
? {
|
||||
authPrompt: '请输入访问令牌(Token):',
|
||||
retry: '重试中...',
|
||||
cancelled: '已取消连接',
|
||||
tokenRequired: '需要有效的访问令牌才能连接'
|
||||
}
|
||||
: {
|
||||
authPrompt: 'Please enter access token:',
|
||||
retry: 'Retrying...',
|
||||
cancelled: 'Connection cancelled',
|
||||
tokenRequired: 'Valid access token required to connect'
|
||||
};
|
||||
|
||||
const token = prompt(t.authPrompt);
|
||||
if (token && token.trim()) {
|
||||
localStorage.setItem('picoclaw-token', token.trim());
|
||||
statusDiv.innerHTML = '<div class="status-dot"></div><span>' + t.retry + '</span>';
|
||||
addMessage('system', t.retry, 'system');
|
||||
setTimeout(connect, 500);
|
||||
} else {
|
||||
addMessage('system', t.cancelled + ' - ' + t.tokenRequired, 'system');
|
||||
addRetryButton();
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(content) {
|
||||
if (!content.trim() || !ws || ws.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@ type WebSocketConfig struct {
|
|||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WEBSOCKET_ENABLED"`
|
||||
Host string `json:"host" env:"PICOCLAW_CHANNELS_WEBSOCKET_HOST"`
|
||||
Port int `json:"port" env:"PICOCLAW_CHANNELS_WEBSOCKET_PORT"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_WEBSOCKET_TOKEN"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WEBSOCKET_ALLOW_FROM"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,8 +115,9 @@ func DefaultConfig() *Config {
|
|||
},
|
||||
WebSocket: WebSocketConfig{
|
||||
Enabled: false,
|
||||
Host: "0.0.0.0",
|
||||
Host: "127.0.0.1",
|
||||
Port: 8080,
|
||||
Token: "YOUR_TOKEN",
|
||||
AllowFrom: FlexibleStringSlice{},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue