completed SMS gateway project

This commit is contained in:
Min Zeya Phyo
2025-10-22 17:22:17 +08:00
commit c883fa7128
190 changed files with 16294 additions and 0 deletions

237
WEBSOCKET_FIX.md Normal file
View 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! 🚀