サイトの命綱であるSSL証明書を簡単にチェックするRubyスクリプトを更に改良しました。

スクリプト内容

このRubyスクリプトの動きを解説してください

#!/usr/bin/env ruby

require 'openssl'
require 'socket'
require 'date'
require 'uri'
require 'timeout'
require 'net/http'

# ANSIカラーコード
COLORS = {
  red:    "\e[31m",
  yellow: "\e[33m",
  green:  "\e[32m",
  reset:  "\e[0m"
}.freeze

# 1. 引数のURL/ドメインを適切にパースするメソッド
def parse_to_uri(input)
  clean_input = input.strip
  url = clean_input.match?(%r{\Ahttps?://}) ? clean_input : "https://#{clean_input}"
  URI.parse(url)
rescue URI::InvalidURIError
  nil
end

# 2. リダイレクトを追跡して最終的なURIを返すメソッド
def fetch_effective_uri(uri, limit = 5)
  return nil if limit == 0

  response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.open_timeout = 5
    http.read_timeout = 5
    http.head(uri.path.empty? ? '/' : uri.path)
  end

  case response
  when Net::HTTPRedirection
    redirect_uri = URI.join(uri, response['location'])
    fetch_effective_uri(redirect_uri, limit - 1)
  else
    uri
  end
rescue => e
  nil
end

# 3. 証明書の有効期限を取得するメソッド(エラー箇所を修正)
def fetch_certificate_expiry(uri)
  Timeout.timeout(5) do
    # ポートが明示されていない場合は、スキーマがhttpsなら443、それ以外なら80にする
    port = uri.port || (uri.scheme == 'https' ? 443 : 80)

    # TCPSocket.open はブロックを渡すと自動クローズしてくれます
    TCPSocket.open(uri.host, port) do |tcp_socket|
      ctx = OpenSSL::SSL::SSLContext.new
      
      # .open ではなく .new を使用し、ブロックの代わりに ensure でクローズする形に修正
      ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
      ssl_socket.hostname = uri.host
      
      begin
        ssl_socket.connect
        cert = ssl_socket.peer_cert
        
        expiration_date = cert.not_after.to_date
        days_remaining = (expiration_date - Date.today).to_i

        [expiration_date, days_remaining]
      ensure
        ssl_socket.close rescue nil
      end
    end
  end
rescue Timeout::Error
  [nil, "サーバーへの接続がタイムアウトしました。"]
rescue => e
  [nil, e.message]
end

# 4. 結果を表示するメソッド
def print_result(uri, expiration_date, days_remaining)
  if expiration_date
    formatted_date = expiration_date.strftime("%Y/%m/%d")
    
    color = case days_remaining
            when ...14 then COLORS[:red]
            when ...30 then COLORS[:yellow]
            else            COLORS[:green]
            end

    puts "サイト #{uri} の有効期限は #{formatted_date} です。#{color}残り #{days_remaining} 日です。#{COLORS[:reset]}"
  else
    puts "#{COLORS[:red]}サイト #{uri} の証明書取得に失敗しました: #{days_remaining}#{COLORS[:reset]}"
  end
end

# メイン処理
def main
  inputs = ARGV.empty? ? [print("チェックしたいサイト(ドメイン/URL)を入力してください: "), gets].last.to_s : ARGV

  inputs.each do |input|
    next if input.strip.empty?

    uri = parse_to_uri(input)
    if uri.nil?
      puts "#{COLORS[:red]}無効な入力です: #{input}#{COLORS[:reset]}"
      next
    end

    puts "Checking: #{uri} ..."
    
    final_uri = fetch_effective_uri(uri)

    if final_uri.nil?
      puts "#{COLORS[:red]}サイト #{uri} にアクセスできませんでした。#{COLORS[:reset]}"
      next
    end

    unless final_uri.scheme == 'https'
      puts "#{COLORS[:yellow]}最終遷移先がHTTPSではないため、スキップします: #{final_uri}#{COLORS[:reset]}"
      next
    end

    expiration_date, days_remaining = fetch_certificate_expiry(final_uri)
    print_result(final_uri, expiration_date, days_remaining)
  end
end

main if __FILE__ == $PROGRAM_NAME

スクリプトの使い方

このスクリプトを実行するには、Rubyがインストールされた環境(MacのターミナルやLinux、Windowsのコマンドプロンプトなど)が必要です。

ステップ1: ファイルに保存する

上記のコードをコピーし、任意の場所にファイルとして保存します。

  • ファイル名(例): check_cert.rb

ステップ2: スクリプトを実行する

使い方は「引数でまとめて指定する」か「実行後に入力する」の2パターンあります。

  • パターンA:引数でドメインを指定して実行(おすすめ)

調べたいサイトをスペース区切りで並べて一気に実行できます。URLでもドメインだけでもOKです。

ruby check_cert.rb google.com github.com https://rubygems.org
  • パターンB:対話形式で実行

引数を何も渡さずに実行すると、画面上で入力を求められます。

ruby check_cert.rb

実行すると チェックしたいサイト(ドメイン/URL)を入力してください: と表示されるので、そこに yahoo.co.jp などを入力してEnterを押します。

実際の動き(出力のイメージ)

スクリプトを実行すると、内部で通信が行われ、結果がターミナルに以下のように表示されます。残り日数に応じて自動で文字に色がつくため、一目で状況がわかります。

Checking: https://google.com ...
サイト https://google.com の有効期限は 2026/07/15 です。残り 50 日です。(←緑色で表示)

Checking: https://example.com ...
サイト https://example.com の有効期限は 2026/06/05 です。残り 10 日です。(←赤色で表示)

Checking: http://http-only-site.com ...
最終遷移先がHTTPSではないため、スキップします: http://http-only-site.com (←黄色で表示)

Checking: https://invalid-domain-xyz.com ...
サイト https://invalid-domain-xyz.com の証明書取得に失敗しました: Failed to open TCP connection to invalid-domain-xyz.com:443(←赤色で表示)

エラーハンドリングの動き

  • リダイレクトがある場合:
    • http://github.com と入力しても、自動的に https://github.com へ転送(リダイレクト)されたことを検知し、最終的なHTTPSのページに対して証明書をチェックします。
  • 接続できない場合:
    • ドメインが間違っていたり、サーバーがダウンしている場合は、プログラムがクラッシュせずに「Failed to open TCP connection(接続に失敗しました)」とエラー理由を教えてくれます。

まとめというか本題

筆者はLinuxターミナルの/etc/update-motd.dに仕込むことで自サイトの証明書更新のタイミングを計っています。何せ、Let's Encryptはわずか90日。更に短くなるというのもほぼ確定しましていますから。

そして、筆者が、ここまで執拗にこれをチェックするのは「私怨」が本質。

自分をかなり追い詰めていき、体と心を大きく崩してしまった前の職場。その元職場が

  • 2020年代であるにもかかわらず常時SSL化していない

という、IT系会社にあるまじき状態を鼻で笑うためというのもこのスクリプト作成のきっかけ。

  • しかも、最近、屋台骨であるWebサービスの証明書が失効してタイムアウトになっている

という更にお察し状態なのはまた別のお話。