VTRyo Blog

一歩ずつ前に進むブログ

h2oでBasic認証を実装する

f:id:vtryo:20180716131850p:plain 死ぬほどハマったので今のうちにメモしておかねばならない(使命感)

手順

1. h2oのコンフィグでmruby.handlerについて記述する 2. /etc/h2o/sites-enabled/mruby/htpasswd.rbを作成する 3. .htpasswdをmd5で作成する

h2oのコンフィグでmruby.handlerについて記述する

/etc/h2o/sites-enabled/Domain.conf

hosts:
"Domain:80":
   listen:
     port: 80
   paths:
     "/":
      mruby.handler: |
        require "htpasswd.rb" 
        Htpasswd.new("/var/www/html/.htpasswd", "realm-name")
      file.dir: /var/www/html

ドキュメントルート配下に.htpasswdを配置したので上記の記述に。

/etc/h2o/sites-enabled/mruby/htpasswd.rbを作成する

ディレクトリがなかったらmrubyを作る。 その下にhtpasswd.rbを作成する。

公式 

etc/h2o/sites-enabled/mruby/htpasswd.rb

# based on public-domain code by cho45
#
# Copyright (c) 2015 DeNA Co., Ltd., Kazuho Oku
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
class Htpasswd

  attr_accessor :path
  attr_accessor :realm

  def initialize(path, realm)
    @path = path
    @realm = realm
  end

  def call(env)
    if /\/\.ht/.match(env['PATH_INFO'])
      return [ 404, { "Content-Type" => "text/plain" }, [ "not found" ] ]
    end
    auth = env['HTTP_AUTHORIZATION']
    if auth
      method, cred = *auth.split(' ')
      if method.casecmp("basic") == 0
        user, pass = cred.unpack("m")[0].split(':', 2)
        begin
          if lookup(user, pass)
            return [ 399, { "x-fallthru-set-remote-user" => user }, [] ]
          end
        rescue => e
          $stderr.puts "failed to validate password using file:#{@path}:#{e.message}"
          return [ 500, { "Content-Type" => "text/plain" }, [ "Internal Server Error" ] ]
        end
      end
    end
    return [ 401, { "Content-Type" => "text/plain", "WWW-Authenticate" => "Basic realm=\"#{@realm}\"" }, [ "Authorization Required" ] ]
  end

  def lookup(user, pass)
    File.open(@path) do |file|
      file.each_line do |line|
        line_user, hash = line.chomp.split(':', 2)
        if user == line_user && self.class.validate(pass, hash)
          return true
        end
      end
    end
    return false
  end

  def Htpasswd.crypt_md5(pass, salt)
    ctx = Digest::MD5.new.update("#{pass}$apr1$#{salt}")
    final = Digest::MD5.new.update("#{pass}#{salt}#{pass}").digest!.bytes

    l = pass.length
    while l > 0
      ctx.update(final[0 .. (l > 16 ? 16 : l) - 1].pack("C*"))
      l -= 16
    end

    l = pass.length
    while l > 0
      ctx.update(l % 2 != 0 ? "\0" : pass[0])
      l >>= 1
    end

    final = ctx.digest!

    1000.times do |i|
      ctx = Digest::MD5.new
      ctx.update(i % 2 != 0 ? pass : final)
      ctx.update(salt) if i % 3 != 0
      ctx.update(pass) if i % 7 != 0
      ctx.update(i % 2 != 0 ? final : pass)
      final = ctx.digest!
    end

    final = final.bytes
    hash = ""
    for a, b, c in [[0, 6, 12], [1, 7, 13], [2, 8, 14], [3, 9, 15], [4, 10, 5]]
      hash << _to64(final[a] << 16 | final[b] << 8 | final[c], 4)
    end
    hash << _to64(final[11], 2)

    "$apr1$#{salt}$#{hash}"
  end

  def Htpasswd._to64(v, n)
    chars = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    output = ""
    n.times do
      output << chars[v & 0x3f]
      v >>= 6
    end
    output
  end

  def Htpasswd.crypt_sha1(pass)
    "{SHA}" + [Digest::SHA1.new.update(pass).digest!].pack("m").chomp
  end

  def Htpasswd.validate(pass, hash)
    if /^\$apr1\$(.*)\$/.match(hash)
      encoded = crypt_md5(pass, $1)
    elsif /^{SHA}/.match(hash)
      encoded = crypt_sha1(pass)
    else
      raise "crypt-style password hash is not supported"
    end
    return encoded == hash
  end

end

.htpasswdをmd5で作成する

そして最もハマったのがこれ。 何度.htpasswdを作成しても、server internal errorが表示された。

/var/log/h2o/error.log

failed to validate password using file:/var/www/html/.htpasswd:crypt-style password hash is not supported

ハッシュがサポートしていないことはわかったが、そこからが長かった。 パスワードは以下のコマンドで作成しないと読み込んでくれないことがわかった。

htpasswd -nbm myName myPassword

以下それを見つけた記事。

katsyoshiのめもみたいなもの Basic Authenticate in H2o

上記の記事、一番下の注釈2にあるURLに飛んだ。

おわりに

h2o難しすぎるでしょ。。。(笑) ご協力頂いた方に感謝もこめて、忘れぬようめもめも