Files
MySMSAPio/WEBSOCKET_SETUP.md
2025-10-22 17:22:17 +08:00

11 KiB

WebSocket Connection Setup

Issue Fixed

Problem: Android gateway device couldn't connect to WebSocket server

Error:

Request origin not allowed
Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)

Root Cause: Action Cable was blocking WebSocket connections from devices on the local network (192.168.x.x) due to origin restrictions.

Solution Applied

Configuration Changes

File: config/environments/development.rb

Added the following configuration:

# Allow Action Cable connections from any origin in development
config.action_cable.disable_request_forgery_protection = true

# Allow WebSocket connections from local network
config.action_cable.allowed_request_origins = [
  /http:\/\/localhost.*/,
  /http:\/\/127\.0\.0\.1.*/,
  /http:\/\/192\.168\..*/,  # Local network (192.168.0.0/16)
  /http:\/\/10\..*/,         # Local network (10.0.0.0/8)
  /http:\/\/172\.(1[6-9]|2[0-9]|3[0-1])\..*/  # Local network (172.16.0.0/12)
]

What This Does

  1. Disables CSRF protection for Action Cable in development

    • Allows WebSocket connections without origin validation
    • Safe for development environment
    • Should NOT be used in production
  2. Allows specific origins:

    • localhost and 127.0.0.1 (local machine)
    • 192.168.x.x (most common home/office networks)
    • 10.x.x.x (enterprise networks, Docker)
    • 172.16-31.x.x (Docker bridge networks)

How to Apply

  1. Restart Rails server:
# Stop current server (Ctrl+C or kill process)
lsof -ti:3000 | xargs kill -9

# Start server again
bin/dev
  1. Verify Redis is running:
redis-cli ping
# Should return: PONG

If Redis is not running:

# macOS (with Homebrew)
brew services start redis

# Linux
sudo systemctl start redis

# Or run manually
redis-server

Testing WebSocket Connection

Using wscat (Command Line)

Install wscat:

npm install -g wscat

Test connection:

# Replace with your actual API key
wscat -c "ws://localhost:3000/cable?api_key=gw_live_your_key_here"

Expected output:

{"type":"welcome"}

Using Android App

  1. Get API key from admin interface (when creating gateway)
  2. Configure in Android app:
    • API Base URL: http://192.168.x.x:3000 (your computer's IP)
    • WebSocket URL: ws://192.168.x.x:3000/cable
    • API Key: gw_live_...
  3. Connect in the app
  4. Check Rails logs for connection message:
Gateway device-001 connected

Finding Your Computer's IP Address

macOS:

ipconfig getifaddr en0  # WiFi
ipconfig getifaddr en1  # Ethernet

Linux:

hostname -I | awk '{print $1}'

Windows:

ipconfig | findstr IPv4

Connection Flow

1. Client Connects

Android app initiates WebSocket connection:

ws://192.168.x.x:3000/cable?api_key=gw_live_abc123...

2. Server Authenticates

File: app/channels/application_cable/connection.rb

def connect
  self.current_gateway = find_verified_gateway
  logger.info "Gateway #{current_gateway.device_id} connected"
end

private

def find_verified_gateway
  api_key = request.params[:api_key]
  return reject_unauthorized_connection if api_key.blank?

  api_key_digest = Digest::SHA256.hexdigest(api_key)
  gateway = Gateway.find_by(api_key_digest: api_key_digest, active: true)

  gateway || reject_unauthorized_connection
end

3. Welcome Message

Server sends welcome:

{"type":"welcome"}

4. Gateway Subscribes

Android app subscribes to GatewayChannel:

{
  "command": "subscribe",
  "identifier": "{\"channel\":\"GatewayChannel\"}"
}

5. Server Confirms

{
  "identifier": "{\"channel\":\"GatewayChannel\"}",
  "type": "confirm_subscription"
}

6. Communication Begins

Gateway can now:

  • Send heartbeats
  • Report received SMS
  • Report delivery status
  • Receive SMS to send

Production Configuration

Important: Different Settings for Production

File: config/environments/production.rb

DO NOT disable CSRF protection in production. Instead, specify exact allowed origins:

# Production settings (recommended)
config.action_cable.allowed_request_origins = [
  'https://yourdomain.com',
  'https://www.yourdomain.com'
]

# Or allow all HTTPS origins (less secure)
config.action_cable.allowed_request_origins = /https:.*/

# NEVER do this in production:
# config.action_cable.disable_request_forgery_protection = true

Production WebSocket URL

Use wss:// (WebSocket Secure) instead of ws://:

wss://api.yourdomain.com/cable

SSL/TLS Requirements

  1. Enable SSL in Rails:
# config/environments/production.rb
config.force_ssl = true
  1. Configure Puma for SSL:
# config/puma.rb
ssl_bind '0.0.0.0', '3000', {
  key: '/path/to/server.key',
  cert: '/path/to/server.crt'
}
  1. Or use reverse proxy (recommended):
    • Nginx or Apache with SSL
    • Cloudflare
    • AWS Application Load Balancer
    • Heroku (automatic SSL)

Troubleshooting

Connection Refused

Error: Failed to upgrade to WebSocket

Causes:

  1. Rails server not running
  2. Redis not running
  3. Wrong port
  4. Firewall blocking

Solutions:

# Check if server is running
lsof -i:3000

# Check if Redis is running
redis-cli ping

# Check Rails logs
tail -f log/development.log

# Restart server
bin/dev

Authentication Failed

Error: Unauthorized

Causes:

  1. Wrong API key
  2. Gateway not active
  3. API key format incorrect

Solutions:

# Test API key in console
bin/rails console
api_key = "gw_live_abc123..."
digest = Digest::SHA256.hexdigest(api_key)
gateway = Gateway.find_by(api_key_digest: digest)
puts gateway&.device_id || "NOT FOUND"
puts "Active: #{gateway&.active}"

Origin Not Allowed (in production)

Error: Request origin not allowed

Cause: Origin not in allowed_request_origins

Solution: Add your domain to allowed origins:

config.action_cable.allowed_request_origins = [
  'https://yourdomain.com',
  'https://api.yourdomain.com'
]

Connection Drops Frequently

Causes:

  1. Network instability
  2. Server restarting
  3. Redis connection issues
  4. Heartbeat timeout

Solutions:

  1. Implement reconnection in Android app
  2. Monitor Redis:
redis-cli
> CLIENT LIST
> MONITOR
  1. Check heartbeat interval: Gateway should send heartbeat every < 2 minutes
  2. Review server logs for errors

Monitoring WebSocket Connections

View Active Connections

In Rails console:

ActionCable.server.connections.size

Monitor Redis

redis-cli
> CLIENT LIST | grep cable
> SUBSCRIBE sms_gateway_development*

Check Gateway Status

# In Rails console
Gateway.online.each do |g|
  puts "#{g.name}: Last heartbeat #{g.last_heartbeat_at}"
end

View Connection Logs

# Development logs
tail -f log/development.log | grep -i "cable\|gateway\|websocket"

# Production logs
tail -f log/production.log | grep -i "cable\|gateway\|websocket"

Security Considerations

Development (Current Setup)

Acceptable:

  • CSRF protection disabled
  • All origins allowed
  • HTTP connections
  • Local network only

⚠️ Not for production

Production Requirements

🔒 Must Have:

  • HTTPS/WSS only (config.force_ssl = true)
  • Specific allowed origins
  • CSRF protection enabled
  • Rate limiting on connections
  • Authentication required
  • Connection monitoring
  • Audit logging

Best Practices

  1. API Key Security:

    • Never log API keys
    • Use environment variables
    • Rotate keys regularly
    • Revoke compromised keys immediately
  2. Connection Limits:

    • Limit connections per gateway
    • Implement backoff strategy
    • Monitor connection attempts
    • Alert on suspicious activity
  3. Network Security:

    • Use VPN for remote access
    • Implement IP whitelisting
    • Use SSL/TLS certificates
    • Enable firewall rules

Example Android App Code

Connecting to WebSocket

import okhttp3.*
import java.util.concurrent.TimeUnit

class GatewayWebSocket(
    private val apiKey: String,
    private val websocketUrl: String
) {
    private var webSocket: WebSocket? = null
    private val client = OkHttpClient.Builder()
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .pingInterval(30, TimeUnit.SECONDS)
        .build()

    fun connect() {
        val request = Request.Builder()
            .url("$websocketUrl?api_key=$apiKey")
            .build()

        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.d("WebSocket", "Connected!")
                subscribe()
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                Log.d("WebSocket", "Received: $text")
                handleMessage(text)
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e("WebSocket", "Connection failed: ${t.message}")
                reconnect()
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                Log.d("WebSocket", "Closed: $reason")
                reconnect()
            }
        })
    }

    private fun subscribe() {
        val subscribeMessage = """
            {
                "command": "subscribe",
                "identifier": "{\"channel\":\"GatewayChannel\"}"
            }
        """.trimIndent()
        webSocket?.send(subscribeMessage)
    }

    private fun handleMessage(json: String) {
        val message = JSONObject(json)
        when (message.optString("type")) {
            "welcome" -> Log.d("WebSocket", "Welcome received")
            "ping" -> Log.d("WebSocket", "Ping received")
            "confirm_subscription" -> Log.d("WebSocket", "Subscribed to GatewayChannel")
        }
    }

    fun disconnect() {
        webSocket?.close(1000, "Normal closure")
    }

    private fun reconnect() {
        Handler(Looper.getMainLooper()).postDelayed({
            connect()
        }, 5000) // Reconnect after 5 seconds
    }
}

Summary

Fixed: WebSocket connection now works from local network Configuration: Action Cable allows connections from 192.168.x.x, 10.x.x.x, etc. Security: CSRF protection disabled for development only Production: Different, more secure settings required

Next Steps:

  1. Restart your Rails server
  2. Ensure Redis is running
  3. Try connecting from Android app
  4. Check Rails logs for "Gateway XXX connected" message

The WebSocket server is now ready to accept connections from your Android gateway devices on the local network! 🚀