使用 Puma 和 Nginx 配置调试 Action Cable

Debug Action Cable with Puma and Nginx configuration

Action Cable 在开发中有效,但在生产中无效。我不知道如何调试它或如何缩小问题范围。在生产中,我在 Debian 9 系统上使用 puma (3.12.0)、Nginx (1.10.3)、Redis (3.2.6) 和 Rails 5.2.2 (Ruby 2.5.3p105)。除了 Action Cable 以外的所有东西都工作正常。

rails 生产日志以这些行结尾:

I, [...]  INFO -- : [ae5f4486-eadd-466d-a4de-fd8db3cdcfb4] Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: upgrade, HTTP_UPGRADE: websocket)
D, [...] DEBUG -- :   ESC[1mESC[36mUser Load (0.8ms)ESC[0m  ESC[1mESC[34mSELECT  "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ESC[0m  [["LIMIT", 1]]
E, [...] ERROR -- : An unauthorized connection attempt was rejected
E, [...] ERROR -- : Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: upgrade, HTTP_UPGRADE: websocket)
I, [...]  INFO -- : Finished "/cable/" [WebSocket] for 127.0.0.1 at 2019-01-14 15:52:53 +0100
I, [...]  INFO -- : Finished "/cable/" [WebSocket] for 127.0.0.1 at 2019-01-14 15:52:53 +0100
I, [...]  INFO -- : [58aa5ff7-e440-4050-88b6-6e9dcdc691a0] Started GET "/cable" for 127.0.0.1 at 2019-01-14 15:55:35 +0100
I, [...]  INFO -- : [58aa5ff7-e440-4050-88b6-6e9dcdc691a0] Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2019-01-14 15:55:35 +0100
I, [...]  INFO -- : [58aa5ff7-e440-4050-88b6-6e9dcdc691a0] Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: upgrade, HTTP_UPGRADE: websocket)
D, [...] DEBUG -- :   ESC[1mESC[36mUser Load (0.8ms)ESC[0m  ESC[1mESC[34mSELECT  "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ESC[0m  [["LIMIT", 1]]
E, [...] ERROR -- : An unauthorized connection attempt was rejected
E, [...] ERROR -- : Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: upgrade, HTTP_UPGRADE: websocket)
I, [...]  INFO -- : Finished "/cable/" [WebSocket] for 127.0.0.1 at 2019-01-14 15:55:35 +0100
I, [...]  INFO -- : Finished "/cable/" [WebSocket] for 127.0.0.1 at 2019-01-14 15:55:35 +0100

Nginx 配置:

upstream my_app {
  server unix:/tmp/example.sock;
}

server {
  listen 443 ssl;
  # ... ssl configuration

  server_name xyz.example.com;

  root /var/www/example/current/public;

  location /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location / {
    try_files $uri @ruby;
  }

  location @ruby {
    proxy_pass http://my_app;

    proxy_set_header  Host $host;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto $scheme;
    proxy_set_header  X-Forwarded-Ssl on; # Optional
    proxy_set_header  X-Forwarded-Port $server_port;
    proxy_set_header  X-Forwarded-Host $host;

    proxy_redirect off;
  }

  location /cable {
    proxy_pass http://my_app;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

production.rb 我有这些行:

config.action_cable.url = 'wss://xyz.example.com/cable'
config.action_cable.disable_request_forgery_protection = true

app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      # This is a websocket so we have no warden and no session here
      # How to reuse the login made with devise?
      # http://www.rubytutorial.io/actioncable-devise-authentication/
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      if verified_user = User.find_by(id: cookies.signed["user.id"])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

我在我的开发日志中没有看到 An unauthorized connection attempt was rejected 错误,所以我想这一定是问题所在。但是谷歌搜索该短语没有帮助。

如何缩小问题范围?

在我看来,ActionCable 部分可以正常工作,但存在身份验证错误。 Successfully upgraded to WebSocket 表示它打开了与用户的 websocket 连接,但是 An unauthorized connection attempt was rejected 表示您在代码中的某处拒绝了他们的连接。

检查你的 connection.rb 文件,你在某处调用 reject_unauthorized_connection 吗?

可能的修复

app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected

      def find_verified_user
        if current_user = env['warden'].user
          current_user
        else
          reject_unauthorized_connection
        end
      end
  end
end