Разрешить межсайтовые запросы только для шлюза сообщений

Я пытаюсь реализовать виджет чата, который можно встроить на любой сайт, и для этого решил использовать MessageBus для связи между виджетом и моим бэкендом на Rails. Поскольку его можно встроить с любого домена, мне пришлось решить проблему междоменных запросов.

Однако я хочу включить CORS только для запросов MessageBus, а не для всех остальных маршрутов. Я уже видел эту проблему: CORS configuration · Issue #135 · discourse/message_bus · GitHub

Вот что я сделал в файле config/initializers/cors.rb:

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options]
  end
end

Это работает, но позволяет междоменные запросы для любого маршрута, что мне не нужно.

Я также пробовал написать собственное промежуточное ПО:

# app/middleware/message_bus_cors_middleware.rb
class MessageBusCorsMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    # Если это MessageBus и запрос OPTIONS, возвращаем заголовки CORS
    if env['PATH_INFO'].start_with?('/message-bus') && env['REQUEST_METHOD'] == 'OPTIONS'
      # Применяем заголовки CORS для запросов /message-bus
      headers = {
        'Access-Control-Allow-Origin' => '*',
        'Access-Control-Allow-Methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD',
        'Access-Control-Allow-Headers' => 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Visitor-Token',
        'Access-Control-Max-Age' => '86400'
      }
      [200, headers, []]
    else
      @app.call(env)
    end
  end
end

Но с этим промежуточным ПО у меня всё ещё возникают проблемы с междоменными запросами.

Есть ли другие идеи?

Ещё одна вещь, которая была бы полезна: включить междоменные запросы только для конкретных каналов (в моём случае — каналов чата MessageBus). Я использую MessageBus и в других частях приложения, где междоменные запросы не требуются, и хочу оставить их без изменений.

Мне удалось заставить это работать с помощью второго метода (кастомный middleware для включения CORS только для запросов Message Bus). Проблема заключалась в заголовке access-control-allow-headers, который был установлен неправильно. Исправление состояло в том, чтобы заполнить ответный заголовок access-control-allow-headers значениями из заголовков запроса Access-Control-Request-Headers.

Вот итоговая реализация:

class MessageBusCorsMiddleware
  HTTP_ORIGIN = 'HTTP_ORIGIN'
  HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'

  HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'
  HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
  OPTIONS = 'OPTIONS'
  REQUEST_METHOD = 'REQUEST_METHOD'
  PATH_INFO      = 'PATH_INFO'

  def initialize(app)
    @app = app
  end

  def call(env)
    # Если это Message Bus и запрос OPTIONS, возвращаем заголовки CORS
    if env[PATH_INFO].start_with?('/message-bus') && env[REQUEST_METHOD] == OPTIONS && env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
      origin = env['HTTP_ORIGIN']
      # Применяем заголовки CORS для запросов к /message-bus
      headers = {
        "access-control-allow-origin" => "*",
        "access-control-allow-methods" => "GET, POST, PUT, PATCH, DELETE, OPTIONS",
        "access-control-expose-headers" => "",
        "access-control-max-age" => "7200"
      }

      headers.merge!('access-control-allow-headers' => env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]) if env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
      [200, headers, []]
    else
      @app.call(env)
    end
  end
end

У меня возникли трудности с пониманием роли заголовка HTTP “Access-Control-Request-Headers”, поэтому я спросил ChatGPT:

Заголовок HTTP “Access-Control-Request-Headers” используется в контексте запросов Cross-Origin Resource Sharing (CORS). CORS — это механизм, позволяющий веб-приложениям отправлять запросы на домен, отличный от того, с которого было запущено приложение.

Когда веб-приложение отправляет кросс-доменный запрос, оно обычно сначала отправляет серверу предварительный запрос (preflight), чтобы определить, разрешён ли фактический запрос. В предварительном запросе содержится заголовок “Access-Control-Request-Headers”, в котором перечислены дополнительные заголовки, которые приложение хочет включить в фактический запрос.

Например, если веб-приложение хочет включить в кросс-доменный запрос пользовательские заголовки, такие как “X-Auth-Token” или “Authorization”, оно указывает эти заголовки в заголовке “Access-Control-Request-Headers” предварительного запроса.

Сервер, получивший предварительный запрос, может проверить заголовок “Access-Control-Request-Headers”, чтобы определить, разрешены ли запрашиваемые заголовки для фактического запроса. Если сервер одобряет запрашиваемые заголовки, он отвечает соответствующим заголовком Access-Control-Allow-Headers в ответе на предварительный запрос, позволяя веб-приложению продолжить фактический запрос.

Иными словами, “Access-Control-Request-Headers” — это заголовок HTTP, используемый в предварительных запросах CORS для указания дополнительных заголовков, которые веб-приложение хочет включить в свой кросс-доменный запрос.

Я делюсь этим на случай, если кто-то ещё пытается достичь того же результата. Я также открыт к обратной связи, особенно в отношении проблем безопасности, которые может открыть данная реализация.

Вы можете добавить дополнительные заголовки ответа согласно документации:

Например, в config/initializers/message_bus.rb:

MessageBus.extra_response_headers_lookup do |env|
  [
    ["Access-Control-Allow-Origin", "http://example.com:3000"],
  ]
end

Я забыл упомянуть, но это было первое, что я попробовал, и это не сработало. Но это было до того, как я узнал об Access-Control-Allow-Headers. Возможно, это сработает, если я добавлю их вот так.


MessageBus.extra_response_headers_lookup do |env|
 [
    ["Access-Control-Allow-Origin", "http://example.com:3000"],
    ["Access-Control-Allow-Headers", env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]]
  ]
end

Я попробую снова таким способом вместо кастомного middleware и обновлю эту ветку.

И с этим тоже не повезло. Пока что я оставил пользовательское промежуточное ПО.