completed SMS gateway project
This commit is contained in:
237
WEBSOCKET_FIX.md
Normal file
237
WEBSOCKET_FIX.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# 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! 🚀
|
||||
Reference in New Issue
Block a user