238 lines
6.5 KiB
Markdown
238 lines
6.5 KiB
Markdown
# WebSocket Subscription Rejection Fix
|
|
|
|
## Issue
|
|
**Error**: "GatewayChannel is transmitting the subscription rejection"
|
|
|
|
## Root Cause
|
|
|
|
The `GatewayChannel#subscribed` method was attempting to re-authenticate the gateway using `params[:api_key_digest]`, but this caused two problems:
|
|
|
|
1. **Double Authentication**: The gateway was already authenticated at the connection level in `ApplicationCable::Connection#connect`
|
|
2. **Missing Parameter**: The channel was expecting `api_key_digest` as a channel parameter, but it was never being passed
|
|
3. **Wrong Layer**: Authentication should happen at the connection level (before subscription), not at the channel subscription level
|
|
|
|
## Authentication Flow
|
|
|
|
### Correct Flow (After Fix)
|
|
|
|
```
|
|
1. Client connects to WebSocket
|
|
URL: ws://host/cable?api_key=gw_live_...
|
|
|
|
2. ApplicationCable::Connection#connect
|
|
- Extracts api_key from query params
|
|
- Hashes it with SHA256
|
|
- Finds gateway by api_key_digest
|
|
- Sets current_gateway
|
|
- Connection accepted ✅
|
|
|
|
3. Client subscribes to GatewayChannel
|
|
Message: {"command":"subscribe","identifier":"{\"channel\":\"GatewayChannel\"}"}
|
|
|
|
4. GatewayChannel#subscribed
|
|
- Uses current_gateway (already authenticated)
|
|
- Streams to gateway-specific channel
|
|
- Updates heartbeat
|
|
- Subscription accepted ✅
|
|
```
|
|
|
|
### Previous Flow (Before Fix)
|
|
|
|
```
|
|
1. Client connects to WebSocket
|
|
URL: ws://host/cable?api_key=gw_live_...
|
|
|
|
2. ApplicationCable::Connection#connect
|
|
- Authenticates gateway
|
|
- Connection accepted ✅
|
|
|
|
3. Client subscribes to GatewayChannel
|
|
Message: {"command":"subscribe","identifier":"{\"channel\":\"GatewayChannel\"}"}
|
|
|
|
4. GatewayChannel#subscribed (BROKEN)
|
|
- Looks for params[:api_key_digest] ❌ (doesn't exist)
|
|
- Gateway lookup fails ❌
|
|
- Subscription rejected ❌
|
|
```
|
|
|
|
## The Fix
|
|
|
|
### Before (Broken)
|
|
|
|
```ruby
|
|
class GatewayChannel < ApplicationCable::Channel
|
|
def subscribed
|
|
# Authenticate gateway using API key from params
|
|
api_key_digest = params[:api_key_digest] # ❌ This was never passed!
|
|
|
|
unless api_key_digest
|
|
reject # ❌ Always rejected here
|
|
return
|
|
end
|
|
|
|
@gateway = Gateway.find_by(api_key_digest: api_key_digest, active: true)
|
|
|
|
unless @gateway
|
|
reject
|
|
return
|
|
end
|
|
|
|
stream_for @gateway
|
|
@gateway.heartbeat!
|
|
Rails.logger.info("Gateway #{@gateway.device_id} connected via WebSocket")
|
|
end
|
|
end
|
|
```
|
|
|
|
### After (Fixed)
|
|
|
|
```ruby
|
|
class GatewayChannel < ApplicationCable::Channel
|
|
def subscribed
|
|
# Gateway is already authenticated at the connection level
|
|
# current_gateway is set by ApplicationCable::Connection
|
|
@gateway = current_gateway # ✅ Use already-authenticated gateway
|
|
|
|
unless @gateway
|
|
reject
|
|
return
|
|
end
|
|
|
|
# Subscribe to gateway-specific channel
|
|
stream_for @gateway
|
|
|
|
# Update gateway status
|
|
@gateway.heartbeat!
|
|
|
|
Rails.logger.info("Gateway #{@gateway.device_id} subscribed to GatewayChannel")
|
|
end
|
|
end
|
|
```
|
|
|
|
## Key Changes
|
|
|
|
1. **Removed** `params[:api_key_digest]` lookup (it was never passed)
|
|
2. **Use** `current_gateway` which is set by the connection authentication
|
|
3. **Simplified** authentication - it only happens once at the connection level
|
|
4. **Removed** redundant authentication check
|
|
|
|
## Connection Authentication
|
|
|
|
The connection-level authentication in `ApplicationCable::Connection` is the correct place for this:
|
|
|
|
```ruby
|
|
module ApplicationCable
|
|
class Connection < ActionCable::Connection::Base
|
|
identified_by :current_gateway
|
|
|
|
def connect
|
|
self.current_gateway = find_verified_gateway
|
|
logger.info "Gateway #{current_gateway.device_id} connected"
|
|
end
|
|
|
|
private
|
|
|
|
def find_verified_gateway
|
|
# Get API key from request params (query string)
|
|
api_key = request.params[:api_key]
|
|
|
|
if api_key.blank?
|
|
reject_unauthorized_connection
|
|
end
|
|
|
|
# Hash the API key and find gateway
|
|
api_key_digest = Digest::SHA256.hexdigest(api_key)
|
|
gateway = Gateway.find_by(api_key_digest: api_key_digest, active: true)
|
|
|
|
if gateway
|
|
gateway
|
|
else
|
|
reject_unauthorized_connection
|
|
end
|
|
end
|
|
end
|
|
end
|
|
```
|
|
|
|
## Testing the Fix
|
|
|
|
### Test with wscat
|
|
|
|
```bash
|
|
wscat -c "ws://localhost:3000/cable?api_key=gw_live_your_key_here"
|
|
```
|
|
|
|
**After connection**, subscribe to the channel:
|
|
|
|
```json
|
|
{"command":"subscribe","identifier":"{\"channel\":\"GatewayChannel\"}"}
|
|
```
|
|
|
|
**Expected Response**:
|
|
|
|
```json
|
|
{
|
|
"identifier": "{\"channel\":\"GatewayChannel\"}",
|
|
"type": "confirm_subscription"
|
|
}
|
|
```
|
|
|
|
### Test from Android
|
|
|
|
```kotlin
|
|
val wsUrl = "ws://192.168.1.100:3000/cable?api_key=$apiKey"
|
|
val request = Request.Builder().url(wsUrl).build()
|
|
|
|
webSocket = client.newWebSocket(request, object : WebSocketListener() {
|
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
|
// Connection successful
|
|
|
|
// Subscribe to channel
|
|
val subscribeMsg = JSONObject().apply {
|
|
put("command", "subscribe")
|
|
put("identifier", """{"channel":"GatewayChannel"}""")
|
|
}
|
|
webSocket.send(subscribeMsg.toString())
|
|
}
|
|
|
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
val json = JSONObject(text)
|
|
if (json.optString("type") == "confirm_subscription") {
|
|
// Subscription successful! ✅
|
|
Log.d(TAG, "Subscribed to GatewayChannel")
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
## Why This Matters
|
|
|
|
### Before Fix
|
|
- ❌ All subscription attempts were rejected
|
|
- ❌ Gateways couldn't receive SMS send commands
|
|
- ❌ Real-time communication was broken
|
|
- ❌ Messages stuck in "pending" state
|
|
|
|
### After Fix
|
|
- ✅ Subscriptions work correctly
|
|
- ✅ Gateways receive SMS send commands in real-time
|
|
- ✅ Bidirectional communication enabled
|
|
- ✅ Messages sent immediately to online gateways
|
|
|
|
## Related Files
|
|
|
|
- `app/channels/application_cable/connection.rb` - Connection authentication (unchanged, already correct)
|
|
- `app/channels/gateway_channel.rb` - Channel subscription (FIXED)
|
|
- `CABLE_DOCUMENTATION.md` - WebSocket integration guide (already correct)
|
|
- `API_DOCUMENTATION.md` - Full API docs (already correct)
|
|
|
|
## Summary
|
|
|
|
The fix removes redundant authentication from the channel subscription layer and properly uses the `current_gateway` that was already authenticated at the connection level. This follows Action Cable best practices where:
|
|
|
|
1. **Connection authentication** happens when WebSocket connects
|
|
2. **Channel subscription** happens after connection is authenticated
|
|
3. **No re-authentication** needed in channels - use `current_gateway` or other identifiers set at connection level
|
|
|
|
WebSocket connections from Android devices should now work correctly! 🚀
|