Rails 3、HTTP 扩展 (WebDAV) 和 Rack App 安装

发布于 2024-09-29 00:02:39 字数 5218 浏览 7 评论 0 原文

1 下面更多的是向代码开发人员指出一个可能被视为缺陷的 Rails 问题。
2 我也向更了解的人询问一些意见。

我想通过 Warden 身份验证将 WebDAV 添加到我的 Rails 3 应用程序中。我的 Warden 中间件是通过 Devise 注入的。

http://github.com/chrisroberts/dav4rack
http://github.com/hassox/warden
http://github.com/plataformatec/devise

我无法从 Rails 应用程序内部安装 DAV4Rack 处理程序(路线) ,像这样:

# in config/routes.rb
mount DAV4Rack::Handler.new(
  :root => Rails.root.to_s, # <= it's just an example
  :root_uri_path => '/webdav',
  :resource_class => Dav::DocumentResource # <= my custom resource, you could use FileResource from dav4rack
), :at => "/webdav"

因为rails验证HTTP动词(GET POST PUT ..),而webdav使用不验证的HTTP扩展(如PROPFIND),引发以下异常:

ActionController::UnknownHttpMethod (PROPFIND, accepted HTTP methods are get, head, put, post, delete, and options)

此验证发生在ActionDispatch中:

/usr/local/lib/ruby/gems/1.9.1/gems/actionpack-3.0.0/lib/action_dispatch/http/request.rb +56 +72
in (56) "def request_method" and (72) "def method"

来自执行验证的ActionDispatch的示例代码,澄清一下:

def method
  @method ||= begin
    method = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
    HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
    method
  end
end

理论上,我们可以对这个验证进行猴子补丁以符合 webdav 动词,例如 railsdav 项目用来做(注意,那里是rails 2,在rails 3 中需要给action_dispatch/http/request 打猴子补丁)。

要将 DAV4Rack 处理程序添加到 Rails 应用程序中,我必须在机架级别的 ActionDispatch 外部安装处理程序,如下所示:

# config.ru
require ::File.expand_path('../config/environment',  __FILE__)
require 'dav4rack/interceptor'
require 'dav/document_resource'

app = Rack::Builder.new{
  map '/webdav/' do
    run DAV4Rack::Handler.new(
      :root => Rails.root.to_s,
      :root_uri_path => '/webdav',
      :resource_class => Dav::DocumentResource
    )
  end

  map '/' do
    use DAV4Rack::Interceptor, :mappings => {
      '/webdav/' => {
        :resource_class => Dav::DocumentResource
      },
    }
    run Pmp::Application
  end
}.to_app
run app

现在我的应用程序中具有 Webdav 支持。但它仍然需要身份验证,为此我想使用典狱长。

# in document_resource.rb
def check_authentication
  puts request.env['warden'] # nil :(
end

Warden 为零,因为我的 DAV4Rack::Handler 安装在会话和 Warden 中间件之上。 使用“rake middleware”检查我的堆栈,我可以看到以下内容:

> rake middleware 
use ActionDispatch::Static
use Rack::Lock
use ActiveSupport::Cache::Strategy::LocalCache
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActionDispatch::BestStandardsSupport
use Warden::Manager
run Pmp::Application.routes

我相信通过用 DAV 处理程序包装“Pmp::Application.routes”(就像我上面在 config.ru 中对“Pmp::Application”所做的那样)将将我的 webdav 处理程序注入堆栈中的正确位置以满足两个条件:

  1. 位于 ActionDispatch 方法验证代码上方,以避免 ActionController::UnknownHttpMethod
  2. 位于 session 和 Warden::Manager 下方,以便我可以使用 Warden 身份验证。

怎么做呢?看看“rake middleware”otput,似乎很明显要覆盖“Pmp::Application.routes”方法:

# in my app at APP_ROOT/config/application.rb
# override the routes method inherited from Rails::Application#routes
def routes
  routes_app = super
  app = Rack::Builder.new {
    map '/webdav/' do
      run DAV4Rack::Handler.new(
        :root => Rails.root.to_s,
        :root_uri_path => '/webdav',
        :resource_class => Dav::DocumentResource
      )
    end

    map '/' do
      use DAV4Rack::Interceptor, :mappings => {
        '/webdav/' => {
          :resource_class => Dav::DocumentResource
        },
      }
      run routes_app
    end
  }.to_app

  class << app; self end.class_eval do
    attr_accessor :routes_app
    def method_missing(sym, *args, &block)
      routes_app.send sym, *args, &block
    end
  end
  app.routes_app = routes_app

  app
end

因为我们的新机架应用程序“app”将被询问一些旧机架应用程序“routes_app”使用的方法为了响应,我们将这些委托给旧的原始应用程序“routes_app”,并带有一点method_missing魔法。

瞧:一切正常! 巨大的成功。

只有一个问题:我不喜欢它。除了覆盖路由方法之外,必须有更好的方法来完成所有这些封装。

请注意,这不适用于乘客。最好的方法似乎是猴子修补铁轨。
请参阅:dav4rack wiki

大问题:

是否有更好的方法在上面添加 RACK 应用程序通过机架安装或其他方式的“Pmp::Application#routes”应用程序???

主要结论

  1. routes.rb中的“mount”语义应该是机架级的(不是rails/railtie/其他),以允许以这种方式处理HTTP扩展,或者至少有一个针对这种情况的方法“mount_rack”

1 The following is more to point out to the code devs an issue of rails that can be percieved as a flaw.
2 And also me asking some oppinions from people who know better.

I want to add WebDAV to my Rails 3 App with Warden authentication. My warden middleware is injected via Devise.

http://github.com/chrisroberts/dav4rack
http://github.com/hassox/warden
http://github.com/plataformatec/devise

I cannot mount DAV4Rack handlers from inside rails app (routes), like this:

# in config/routes.rb
mount DAV4Rack::Handler.new(
  :root => Rails.root.to_s, # <= it's just an example
  :root_uri_path => '/webdav',
  :resource_class => Dav::DocumentResource # <= my custom resource, you could use FileResource from dav4rack
), :at => "/webdav"

because rails validates HTTP verbs (GET POST PUT ..), and webdav uses HTTP extensions like PROPFIND that do not validate, throwing the following exception:

ActionController::UnknownHttpMethod (PROPFIND, accepted HTTP methods are get, head, put, post, delete, and options)

This validation takes place in ActionDispatch:

/usr/local/lib/ruby/gems/1.9.1/gems/actionpack-3.0.0/lib/action_dispatch/http/request.rb +56 +72
in (56) "def request_method" and (72) "def method"

Sample code from ActionDispatch that does the validation, to make things clear:

def method
  @method ||= begin
    method = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
    HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
    method
  end
end

In theory, we could monkey-patch this validation to comply with webdav verbs like the railsdav project used to do (note that is rails 2 there, in rails 3 one needs to monkey-patch action_dispatch/http/request).

To add DAV4Rack handlers to the rails app I have to mount the handler outside of ActionDispatch, at rack level, like this:

# config.ru
require ::File.expand_path('../config/environment',  __FILE__)
require 'dav4rack/interceptor'
require 'dav/document_resource'

app = Rack::Builder.new{
  map '/webdav/' do
    run DAV4Rack::Handler.new(
      :root => Rails.root.to_s,
      :root_uri_path => '/webdav',
      :resource_class => Dav::DocumentResource
    )
  end

  map '/' do
    use DAV4Rack::Interceptor, :mappings => {
      '/webdav/' => {
        :resource_class => Dav::DocumentResource
      },
    }
    run Pmp::Application
  end
}.to_app
run app

Now I have Webdav support in my application. But It still needs authentication, and for that I'd like to use warden.

# in document_resource.rb
def check_authentication
  puts request.env['warden'] # nil :(
end

Warden is nil because my DAV4Rack::Handler is mounted above the session and warden middleware.
Using "rake middleware" to inspect my stack I can see the following:

> rake middleware 
use ActionDispatch::Static
use Rack::Lock
use ActiveSupport::Cache::Strategy::LocalCache
use Rack::Runtime
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::RemoteIp
use Rack::Sendfile
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::MethodOverride
use ActionDispatch::Head
use ActionDispatch::BestStandardsSupport
use Warden::Manager
run Pmp::Application.routes

I believe that by wrapping "Pmp::Application.routes" with DAV handler (just like I do above for "Pmp::Application" in config.ru) will inject my webdav handler in the stack at the right place to satisfy the two conditions:

  1. Be above ActionDispatch method validation code, to avoid ActionController::UnknownHttpMethod
  2. Be below session and Warden::Manager so I can use warden authentication.

How to do that? Looking at "rake middleware" otput it seems obvious to override the "Pmp::Application.routes" method:

# in my app at APP_ROOT/config/application.rb
# override the routes method inherited from Rails::Application#routes
def routes
  routes_app = super
  app = Rack::Builder.new {
    map '/webdav/' do
      run DAV4Rack::Handler.new(
        :root => Rails.root.to_s,
        :root_uri_path => '/webdav',
        :resource_class => Dav::DocumentResource
      )
    end

    map '/' do
      use DAV4Rack::Interceptor, :mappings => {
        '/webdav/' => {
          :resource_class => Dav::DocumentResource
        },
      }
      run routes_app
    end
  }.to_app

  class << app; self end.class_eval do
    attr_accessor :routes_app
    def method_missing(sym, *args, &block)
      routes_app.send sym, *args, &block
    end
  end
  app.routes_app = routes_app

  app
end

Because our new rack application "app" will be asked a few methods down the chain, that the old rack application "routes_app" used to resopnd to, we delegate theese to the old original application "routes_app" with a little method_missing magic.

And voila: everything is working!
Great success.

Only one problem: I don't like it. There must be a better way to do all this enveloping, other than overriding routes method.

Note that this doesn't work with passenger. The best way seems to be monkey patching rails.
See: dav4rack wiki

THE BIG QUESTION:

IS THERE A BETTER WAY TO ADD A RACK APP JUST ABOVE THE "Pmp::Application#routes" APP BY MEANS OF RACK MOUNT OR OTHER ???

THE BIG CONCLUSION

  1. The "mount" semantics in routes.rb should be rack-level (not rails/railtie/whatever), to allow, in this way, handeling of HTTP extensions, or at least have a method for this case "mount_rack"

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文