feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
This commit is contained in:
@@ -3,7 +3,11 @@ package websocket
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -12,10 +16,62 @@ import (
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow all origins in development
|
||||
return websocketOriginAllowed(r)
|
||||
},
|
||||
}
|
||||
|
||||
func websocketOriginAllowed(r *http.Request) bool {
|
||||
origin := strings.TrimSpace(r.Header.Get("Origin"))
|
||||
if origin == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
allowedOrigins := splitAllowedOrigins(os.Getenv("WEBSOCKET_ALLOWED_ORIGINS"))
|
||||
if len(allowedOrigins) == 0 {
|
||||
return sameOriginHost(origin, r.Host)
|
||||
}
|
||||
|
||||
for _, allowed := range allowedOrigins {
|
||||
if allowed == "*" || strings.EqualFold(allowed, origin) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func splitAllowedOrigins(raw string) []string {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(raw, ",")
|
||||
origins := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
origins = append(origins, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
return origins
|
||||
}
|
||||
|
||||
func sameOriginHost(origin, requestHost string) bool {
|
||||
parsedOrigin, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
originHost := parsedOrigin.Hostname()
|
||||
requestHostname := requestHost
|
||||
if host, _, err := net.SplitHostPort(requestHost); err == nil {
|
||||
requestHostname = host
|
||||
}
|
||||
|
||||
return strings.EqualFold(originHost, requestHostname)
|
||||
}
|
||||
|
||||
// Server represents the WebSocket server
|
||||
type Server struct {
|
||||
clients map[*Client]bool
|
||||
@@ -27,9 +83,9 @@ type Server struct {
|
||||
|
||||
// Client represents a WebSocket client
|
||||
type Client struct {
|
||||
conn *websocket.Conn
|
||||
send chan []byte
|
||||
server *Server
|
||||
conn *websocket.Conn
|
||||
send chan []byte
|
||||
server *Server
|
||||
subscriptions map[string]bool
|
||||
}
|
||||
|
||||
@@ -50,8 +106,9 @@ func (s *Server) Start() {
|
||||
case client := <-s.register:
|
||||
s.mu.Lock()
|
||||
s.clients[client] = true
|
||||
count := len(s.clients)
|
||||
s.mu.Unlock()
|
||||
log.Printf("Client connected. Total clients: %d", len(s.clients))
|
||||
log.Printf("Client connected. Total clients: %d", count)
|
||||
|
||||
case client := <-s.unregister:
|
||||
s.mu.Lock()
|
||||
@@ -59,11 +116,12 @@ func (s *Server) Start() {
|
||||
delete(s.clients, client)
|
||||
close(client.send)
|
||||
}
|
||||
count := len(s.clients)
|
||||
s.mu.Unlock()
|
||||
log.Printf("Client disconnected. Total clients: %d", len(s.clients))
|
||||
log.Printf("Client disconnected. Total clients: %d", count)
|
||||
|
||||
case message := <-s.broadcast:
|
||||
s.mu.RLock()
|
||||
s.mu.Lock()
|
||||
for client := range s.clients {
|
||||
select {
|
||||
case client.send <- message:
|
||||
@@ -72,7 +130,7 @@ func (s *Server) Start() {
|
||||
delete(s.clients, client)
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +247,7 @@ func (c *Client) handleMessage(msg map[string]interface{}) {
|
||||
channel, _ := msg["channel"].(string)
|
||||
c.subscriptions[channel] = true
|
||||
c.sendMessage(map[string]interface{}{
|
||||
"type": "subscribed",
|
||||
"type": "subscribed",
|
||||
"channel": channel,
|
||||
})
|
||||
|
||||
@@ -197,13 +255,13 @@ func (c *Client) handleMessage(msg map[string]interface{}) {
|
||||
channel, _ := msg["channel"].(string)
|
||||
delete(c.subscriptions, channel)
|
||||
c.sendMessage(map[string]interface{}{
|
||||
"type": "unsubscribed",
|
||||
"type": "unsubscribed",
|
||||
"channel": channel,
|
||||
})
|
||||
|
||||
case "ping":
|
||||
c.sendMessage(map[string]interface{}{
|
||||
"type": "pong",
|
||||
"type": "pong",
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
@@ -222,4 +280,3 @@ func (c *Client) sendMessage(msg map[string]interface{}) {
|
||||
close(c.send)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
backend/api/websocket/server_test.go
Normal file
42
backend/api/websocket/server_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWebsocketOriginAllowedDefaultsToSameHost(t *testing.T) {
|
||||
t.Setenv("WEBSOCKET_ALLOWED_ORIGINS", "")
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ws", nil)
|
||||
req.Host = "example.com:8080"
|
||||
req.Header.Set("Origin", "https://example.com")
|
||||
|
||||
if !websocketOriginAllowed(req) {
|
||||
t.Fatal("expected same-host websocket origin to be allowed by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsocketOriginAllowedRejectsCrossOriginByDefault(t *testing.T) {
|
||||
t.Setenv("WEBSOCKET_ALLOWED_ORIGINS", "")
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ws", nil)
|
||||
req.Host = "example.com:8080"
|
||||
req.Header.Set("Origin", "https://attacker.example")
|
||||
|
||||
if websocketOriginAllowed(req) {
|
||||
t.Fatal("expected cross-origin websocket request to be rejected by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebsocketOriginAllowedHonorsExplicitAllowlist(t *testing.T) {
|
||||
t.Setenv("WEBSOCKET_ALLOWED_ORIGINS", "https://app.example, https://ops.example")
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/ws", nil)
|
||||
req.Host = "example.com:8080"
|
||||
req.Header.Set("Origin", "https://ops.example")
|
||||
|
||||
if !websocketOriginAllowed(req) {
|
||||
t.Fatal("expected allowlisted websocket origin to be accepted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user