このサイトのHTTPS化にあたって nginx で書いていた rewrite のルールを h2o の mruby で処理するように変える必要がありました。

しかしベタで書くのも面倒なので、そこそこ機械的な置換ですむような書きかたができるようなライブラリを書いてしのぎました。

mruby.handler

    paths:
      "/":
        proxy.reverse.url: http://localhost:5001
        proxy.preserve-host: ON
        mruby.handler: |
          require "/srv/www/rewrite_rules.rb"
          lambda do |env|
            RewriteRules.rewrite(env) do
              rewrite '/favicon.ico', '/images/favicon.ico', :break
              rewrite '/apple-touch-icon.png', '/images/apple-touch-icon.png', :break

              rewrite %r{^/2005/colors-canvas\.xhtml$}, '/2005/colors-canvas.html', :permanent
              rewrite %r{^/2005/colors-canvas$}, '/2005/colors-canvas.html', :permanent

              rewrite %r{^/logs/latest$}, '/', :permanent
              rewrite %r{^/logs/latest.rdf$}, '/feed', :permanent
              rewrite %r{^/logs/latest.atom$}, '/feed', :permanent
              rewrite %r{^/latest\.rdf$}, '/feed', :permanent
              rewrite %r{^/blog/index\.(rdf|atom)$}, '/feed', :permanent
              rewrite %r{^/logs(/.+?)(\.(rdf|atom))$}, '/feed', :permanent

              rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
              rewrite %r{^/blog(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
              rewrite %r{^/photo$}, '/photo/', :permanent

              rewrite %r{^/(\d\d\d\d/\d\d(/\d\d)?)$}, '/\1/', :permanent
              rewrite %r{^/\d\d\d\d/$}, '/', :redirect

              rewrite %r{^/view-img(/.+?)$}, '\1', :permanent
            end
          end

h2o の path はディレクトリの指定しかできないみたいなので (自動的に末尾スラッシュがついたりする)、rewrite で同時にパス決め打ちのディスパッチも行っています。

rewrite_rules.rb

class RewriteRules
	class RewriteRulesException < Exception
		attr_reader :response
		def initialize(response)
			@response = response
		end
	end

	attr_reader :env
	attr_reader :path

	def self.rewrite(env, &block)
		self.new(env).run(&block)
	end

	def initialize(env, &block)
		@env = env
		@path_orig = "#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
		@path = @path_orig.dup
	end

	def run(&block)
		begin
			instance_eval(&block)
			if @path != @path_orig
				return [ 307, { 'x-reproxy-url' => path }, [ ] ]
			else
				return [ 399, {}, [ ] ]
			end
		rescue RewriteRulesException => e
			return e.response
		end
	end

	def rewrite(regexp, replace, flag=:continue)
		if @path.gsub!(regexp, replace)
			case flag
			when :redirect
				raise RewriteRulesException.new([ 302, { 'Location' => @path }, [ ] ])
			when :permanent
				raise RewriteRulesException.new([ 301, { 'Location' => @path }, [ ] ])
			when :break
				raise RewriteRulesException.new([ 307, { 'x-reproxy-url' => @path }, [ ] ])
			when :continue
				# nothing
			else
				raise "unsupported flag: #{flag}"
			end
		end
	end
end
require 'rspec'

describe RewriteRules do
	it "should treat :permanent as 302 redirect" do
		env = {
			'PATH_INFO' => '/logs/latest'
		}
		expect(RewriteRules.rewrite(env) {
			rewrite %r{^/logs/latest$}, '/', :permanent
			rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
			rewrite %r{^/foobar/}, '/bazbaz/'
		}).to eq( [301, {"Location"=>"/"}, []])
	end

	it "should treat :redirect as 301 redirect" do
		env = {
			'PATH_INFO' => '/logs/piyo.html'
		}
		expect(RewriteRules.rewrite(env) {
			rewrite %r{^/logs/latest$}, '/', :permanent
			rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :redirect
			rewrite %r{^/foobar/}, '/bazbaz/'
		}).to eq([302, {"Location"=>"/piyo"}, []])
	end

	it "should treat as internal proxy by default" do
		env = {
			'PATH_INFO' => '/foobar/baz'
		}
		expect(RewriteRules.rewrite(env) {
			rewrite %r{^/logs/latest$}, '/', :permanent
			rewrite %r{^/logs(/.+?)(\.(x?html|xml|txt))?$}, '\1', :permanent
			rewrite %r{^/foobar/}, '/bazbaz/'
		}).to eq([307, {"x-reproxy-url"=>"/bazbaz/baz"}, []])
	end

	it "can write logic in dsl" do
		env = {
			'PATH_INFO' => '/foobar/baz',
			'XXX' => true,
		}
		expect(RewriteRules.rewrite(env) {
			if env['XXX']
				rewrite %r{^/foobar/}, '/bazbaz/'
			end
		}).to eq([307, {"x-reproxy-url"=>"/bazbaz/baz"}, []])

		env = {
			'PATH_INFO' => '/foobar/baz',
			'XXX' => false
		}
		expect(RewriteRules.rewrite(env) {
			if env['XXX']
				rewrite %r{^/foobar/}, '/bazbaz/'
			end
		}).to eq([399, {}, []])
	end
end

ところで

rewrite ルールが ruby で書けるということは、すなわち自由にテスト可能であることを意味します。Apache の RewriteRule や nginx の rewrite をテストするのはかなり面倒なので、かなり強力で嬉しい感じがします。

  1. トップ
  2. tech
  3. nginx の rewrite ルールっぽく h2o の mruby でリクエストの rewrite を行う
▲ この日のエントリ