Skip to content

Ruby CGI 编程

CGI(Common Gateway Interface,通用网关接口)是Web服务器与外部程序之间交互的标准接口。虽然现代Web开发更多使用框架如Rails、Sinatra等,但了解CGI编程仍然有助于理解Web应用的基本工作原理。Ruby提供了强大的CGI库,使得编写CGI程序变得简单而优雅。本章将详细介绍Ruby中CGI编程的各种方法和最佳实践。

🎯 CGI基础

什么是CGI

CGI是一种标准协议,允许Web服务器执行外部程序来生成动态网页内容。当用户请求一个CGI程序时,Web服务器会启动该程序,将请求数据传递给它,然后将程序的输出返回给用户。

ruby
# 基本的CGI程序
#!/usr/bin/env ruby

require 'cgi'

# 创建CGI对象
cgi = CGI.new

# 输出HTTP头部和HTML内容
cgi.out do
  "<html>
  <head><title>我的第一个CGI程序</title></head>
  <body>
    <h1>Hello, World!</h1>
    <p>当前时间: #{Time.now}</p>
  </body>
  </html>"
end

CGI环境变量

ruby
# 访问CGI环境变量
#!/usr/bin/env ruby

require 'cgi'

cgi = CGI.new

# 获取环境信息
puts "Content-Type: text/html\n\n"
puts "<html><body>"
puts "<h1>CGI环境信息</h1>"
puts "<ul>"
puts "<li>服务器软件: #{ENV['SERVER_SOFTWARE']}</li>"
puts "<li>服务器名称: #{ENV['SERVER_NAME']}</li>"
puts "<li>网关接口: #{ENV['GATEWAY_INTERFACE']}</li>"
puts "<li>服务器协议: #{ENV['SERVER_PROTOCOL']}</li>"
puts "<li>服务器端口: #{ENV['SERVER_PORT']}</li>"
puts "<li>请求方法: #{ENV['REQUEST_METHOD']}</li>"
puts "<li>请求URI: #{ENV['REQUEST_URI']}</li>"
puts "<li>脚本名称: #{ENV['SCRIPT_NAME']}</li>"
puts "<li>路径信息: #{ENV['PATH_INFO']}</li>"
puts "<li>路径翻译: #{ENV['PATH_TRANSLATED']}</li>"
puts "<li>查询字符串: #{ENV['QUERY_STRING']}</li>"
puts "<li>远程主机: #{ENV['REMOTE_HOST']}</li>"
puts "<li>远程地址: #{ENV['REMOTE_ADDR']}</li>"
puts "<li>认证类型: #{ENV['AUTH_TYPE']}</li>"
puts "<li>远程用户: #{ENV['REMOTE_USER']}</li>"
puts "<li>远程标识: #{ENV['REMOTE_IDENT']}</li>"
puts "<li>内容类型: #{ENV['CONTENT_TYPE']}</li>"
puts "<li>内容长度: #{ENV['CONTENT_LENGTH']}</li>"
puts "<li>HTTP用户代理: #{ENV['HTTP_USER_AGENT']}</li>"
puts "<li>HTTP引用页: #{ENV['HTTP_REFERER']}</li>"
puts "</ul>"
puts "</body></html>"

HTTP头部处理

ruby
# 设置HTTP头部
#!/usr/bin/env ruby

require 'cgi'
require 'json'

cgi = CGI.new

# 设置不同的内容类型
case cgi.params['format'][0]
when 'json'
  cgi.out('type' => 'application/json') do
    { message: "Hello, World!", time: Time.now }.to_json
  end
when 'xml'
  cgi.out('type' => 'application/xml') do
    "<?xml version='1.0' encoding='UTF-8'?>
    <response>
      <message>Hello, World!</message>
      <time>#{Time.now}</time>
    </response>"
  end
else
  cgi.out('type' => 'text/html') do
    "<html>
    <head><title>HTML响应</title></head>
    <body>
      <h1>Hello, World!</h1>
      <p>当前时间: #{Time.now}</p>
    </body>
    </html>"
  end
end

# 设置自定义头部
cgi.out(
  'type' => 'text/html',
  'status' => 'OK',
  'cache-control' => 'no-cache',
  'expires' => '0'
) do
  "<html><body><h1>无缓存页面</h1></body></html>"
end

# 重定向
# cgi.out('status' => 'REDIRECT', 'location' => 'http://example.com') { '' }

📥 处理HTTP请求

GET请求处理

ruby
# 处理GET请求参数
#!/usr/bin/env ruby

require 'cgi'
require 'uri'

cgi = CGI.new

# 获取GET参数
name = cgi['name'] || '访客'
age = cgi['age']&.to_i || 0

# 输出HTML页面
cgi.out do
  html = <<-HTML
  <html>
  <head>
    <title>GET请求示例</title>
    <meta charset="UTF-8">
  </head>
  <body>
    <h1>欢迎, #{CGI.escapeHTML(name)}!</h1>
    <p>年龄: #{age > 0 ? age : '未提供'}</p>
    
    <h2>所有GET参数:</h2>
    <ul>
  HTML
  
  cgi.params.each do |key, values|
    html += "<li>#{CGI.escapeHTML(key)}: #{CGI.escapeHTML(values.join(', '))}</li>"
  end
  
  html += <<-HTML
    </ul>
    
    <h2>测试表单:</h2>
    <form method="GET" action="#{ENV['SCRIPT_NAME']}">
      <p>
        <label>姓名: <input type="text" name="name" value="#{CGI.escapeHTML(name)}"></label>
      </p>
      <p>
        <label>年龄: <input type="number" name="age" value="#{age > 0 ? age : ''}"></label>
      </p>
      <p>
        <input type="submit" value="提交">
      </p>
    </form>
  </body>
  </html>
  HTML
  
  html
end

POST请求处理

ruby
# 处理POST请求参数
#!/usr/bin/env ruby

require 'cgi'

cgi = CGI.new

# 检查请求方法
if cgi.request_method == 'POST'
  # 获取POST参数
  username = cgi['username']
  password = cgi['password']
  message = cgi['message']
  
  # 处理表单数据
  response = process_form_data(username, password, message)
else
  # 显示表单
  response = show_form
end

cgi.out do
  "<html>
  <head>
    <title>POST请求示例</title>
    <meta charset=\"UTF-8\">
  </head>
  <body>
    #{response}
  </body>
  </html>"
end

def process_form_data(username, password, message)
  # 简单的表单处理
  if username.nil? || username.empty?
    return "<h1>错误</h1><p>用户名不能为空</p><p><a href=\"#{ENV['SCRIPT_NAME']}\">返回</a></p>"
  end
  
  # 这里应该进行实际的数据处理和验证
  # 比如保存到数据库、发送邮件等
  
  <<-HTML
  <h1>表单提交成功</h1>
  <p>用户名: #{CGI.escapeHTML(username)}</p>
  <p>消息: #{CGI.escapeHTML(message)}</p>
  <p><a href="#{ENV['SCRIPT_NAME']}">再次提交</a></p>
  HTML
end

def show_form
  <<-HTML
  <h1>POST表单示例</h1>
  <form method="POST" action="#{ENV['SCRIPT_NAME']}">
    <p>
      <label>用户名: <input type="text" name="username" required></label>
    </p>
    <p>
      <label>密码: <input type="password" name="password" required></label>
    </p>
    <p>
      <label>消息:<br><textarea name="message" rows="5" cols="50"></textarea></label>
    </p>
    <p>
      <input type="submit" value="提交">
    </p>
  </form>
  HTML
end

文件上传处理

ruby
# 处理文件上传
#!/usr/bin/env ruby

require 'cgi'
require 'tempfile'

cgi = CGI.new

if cgi.request_method == 'POST'
  # 检查是否有文件上传
  if cgi.params['upload'] && !cgi.params['upload'][0].nil?
    uploaded_file = cgi.params['upload'][0]
    
    # 获取文件信息
    filename = uploaded_file.filename
    content_type = uploaded_file.type
    file_data = uploaded_file.read
    
    # 保存文件(实际应用中应该更安全地处理)
    if filename && !filename.empty?
      File.open("uploads/#{filename}", "wb") do |f|
        f.write(file_data)
      end
      
      response = "<h1>文件上传成功</h1>
                  <p>文件名: #{CGI.escapeHTML(filename)}</p>
                  <p>文件类型: #{CGI.escapeHTML(content_type)}</p>
                  <p>文件大小: #{file_data.length} 字节</p>
                  <p><a href=\"#{ENV['SCRIPT_NAME']}\">再次上传</a></p>"
    else
      response = "<h1>错误</h1><p>请选择要上传的文件</p><p><a href=\"#{ENV['SCRIPT_NAME']}\">返回</a></p>"
    end
  else
    response = "<h1>错误</h1><p>没有接收到文件</p><p><a href=\"#{ENV['SCRIPT_NAME']}\">返回</a></p>"
  end
else
  response = show_upload_form
end

cgi.out do
  "<html>
  <head>
    <title>文件上传示例</title>
    <meta charset=\"UTF-8\">
  </head>
  <body>
    #{response}
  </body>
  </html>"
end

def show_upload_form
  <<-HTML
  <h1>文件上传</h1>
  <form method="POST" action="#{ENV['SCRIPT_NAME']}" enctype="multipart/form-data">
    <p>
      <label>选择文件: <input type="file" name="upload" accept="image/*,.pdf,.txt"></label>
    </p>
    <p>
      <input type="submit" value="上传">
    </p>
  </form>
  
  <h2>注意事项:</h2>
  <ul>
    <li>请确保服务器配置允许文件上传</li>
    <li>检查php.ini或相应的服务器配置文件中的upload_max_filesize和post_max_size设置</li>
    <li>在生产环境中,应该对上传的文件进行安全检查</li>
  </ul>
  HTML
end

📤 生成HTTP响应

HTML响应生成

ruby
# 动态HTML生成
#!/usr/bin/env ruby

require 'cgi'
require 'erb'

cgi = CGI.new

# 使用ERB模板
template = <<-TEMPLATE
<html>
<head>
  <title><%= title %></title>
  <meta charset="UTF-8">
  <style>
    body { font-family: Arial, sans-serif; margin: 40px; }
    .header { background-color: #f0f0f0; padding: 20px; border-radius: 5px; }
    .content { margin-top: 20px; }
    .item { border-bottom: 1px solid #ddd; padding: 10px 0; }
  </style>
</head>
<body>
  <div class="header">
    <h1><%= title %></h1>
    <p>当前时间: <%= current_time %></p>
  </div>
  
  <div class="content">
    <h2>产品列表</h2>
    <% products.each do |product| %>
      <div class="item">
        <h3><%= product[:name] %></h3>
        <p>价格: ¥<%= product[:price] %></p>
        <p><%= product[:description] %></p>
      </div>
    <% end %>
  </div>
</body>
</html>
TEMPLATE

# 数据
title = "我的在线商店"
current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
products = [
  { name: "苹果", price: 5.50, description: "新鲜红富士苹果" },
  { name: "香蕉", price: 3.00, description: "进口香蕉" },
  { name: "橙子", price: 4.20, description: "甜橙" }
]

# 渲染模板
erb = ERB.new(template)
html_content = erb.result(binding)

cgi.out('type' => 'text/html') do
  html_content
end

JSON和XML响应

ruby
# API响应生成
#!/usr/bin/env ruby

require 'cgi'
require 'json'
require 'rexml/document'

cgi = CGI.new

# 模拟数据
users = [
  { id: 1, name: "张三", email: "zhangsan@example.com", age: 25 },
  { id: 2, name: "李四", email: "lisi@example.com", age: 30 },
  { id: 3, name: "王五", email: "wangwu@example.com", age: 28 }
]

# 根据请求格式返回不同响应
format = cgi['format'] || 'json'

case format.downcase
when 'xml'
  # 生成XML响应
  xml_doc = REXML::Document.new
  root = xml_doc.add_element('users')
  
  users.each do |user|
    user_element = root.add_element('user')
    user_element.add_element('id').text = user[:id].to_s
    user_element.add_element('name').text = user[:name]
    user_element.add_element('email').text = user[:email]
    user_element.add_element('age').text = user[:age].to_s
  end
  
  cgi.out('type' => 'application/xml') do
    xml_doc.to_s
  end
  
when 'json'
  # 生成JSON响应
  cgi.out('type' => 'application/json') do
    users.to_json
  end
  
else
  # 生成HTML响应
  html_content = "<html>
  <head>
    <title>用户API</title>
    <meta charset=\"UTF-8\">
  </head>
  <body>
    <h1>用户列表</h1>
    <ul>
  "
  
  users.each do |user|
    html_content += "<li>#{user[:name]} (#{user[:email]}, #{user[:age]}岁)</li>"
  end
  
  html_content += "</ul>
    <p>
      <a href=\"?format=json\">JSON格式</a> |
      <a href=\"?format=xml\">XML格式</a>
    </p>
  </body>
  </html>"
  
  cgi.out('type' => 'text/html') do
    html_content
  end
end

🎯 实用CGI应用示例

简单的访客留言簿

ruby
# 访客留言簿CGI程序
#!/usr/bin/env ruby

require 'cgi'
require 'json'
require 'time'

cgi = CGI.new

# 留言文件路径
GUESTBOOK_FILE = 'guestbook.json'

# 确保留言文件存在
def ensure_guestbook_file
  unless File.exist?(GUESTBOOK_FILE)
    File.write(GUESTBOOK_FILE, '[]')
  end
end

# 读取留言
def read_messages
  ensure_guestbook_file
  JSON.parse(File.read(GUESTBOOK_FILE))
rescue
  []
end

# 保存留言
def save_message(message)
  messages = read_messages
  messages.unshift(message)  # 添加到开头
  messages = messages.first(50)  # 只保留最近50条
  File.write(GUESTBOOK_FILE, JSON.pretty_generate(messages))
end

# 处理表单提交
if cgi.request_method == 'POST'
  name = cgi['name'].to_s.strip
  email = cgi['email'].to_s.strip
  message = cgi['message'].to_s.strip
  
  if name.empty? || message.empty?
    error_message = "姓名和留言内容不能为空"
    response = show_guestbook_form(error_message)
  else
    # 保存留言
    new_message = {
      'id' => Time.now.to_i,
      'name' => name,
      'email' => email,
      'message' => message,
      'timestamp' => Time.now.iso8601
    }
    
    save_message(new_message)
    response = show_guestbook_with_messages("留言保存成功!")
  end
else
  response = show_guestbook_with_messages
end

cgi.out('type' => 'text/html') do
  response
end

def show_guestbook_form(error_message = nil)
  error_html = error_message ? "<p style=\"color: red;\">#{CGI.escapeHTML(error_message)}</p>" : ""
  
  <<-HTML
  <!DOCTYPE html>
  <html>
  <head>
    <title>访客留言簿</title>
    <meta charset="UTF-8">
    <style>
      body { font-family: Arial, sans-serif; margin: 40px; max-width: 800px; }
      .form-group { margin-bottom: 15px; }
      label { display: block; margin-bottom: 5px; font-weight: bold; }
      input, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
      textarea { height: 100px; resize: vertical; }
      button { background-color: #007cba; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
      button:hover { background-color: #005a87; }
      .error { color: red; margin-bottom: 15px; }
    </style>
  </head>
  <body>
    <h1>访客留言簿</h1>
    #{error_html}
    <form method="POST" action="#{ENV['SCRIPT_NAME']}">
      <div class="form-group">
        <label for="name">姓名 *</label>
        <input type="text" id="name" name="name" required>
      </div>
      
      <div class="form-group">
        <label for="email">邮箱</label>
        <input type="email" id="email" name="email">
      </div>
      
      <div class="form-group">
        <label for="message">留言内容 *</label>
        <textarea id="message" name="message" required></textarea>
      </div>
      
      <button type="submit">提交留言</button>
    </form>
    
    <p style="margin-top: 30px;"><a href="#{ENV['SCRIPT_NAME']}">查看留言</a></p>
  </body>
  </html>
  HTML
end

def show_guestbook_with_messages(success_message = nil)
  messages = read_messages
  
  success_html = success_message ? "<p style=\"color: green;\">#{CGI.escapeHTML(success_message)}</p>" : ""
  
  messages_html = ""
  messages.each do |msg|
    timestamp = Time.parse(msg['timestamp']).strftime("%Y-%m-%d %H:%M")
    email_html = msg['email'].empty? ? "" : "<br><small>邮箱: #{CGI.escapeHTML(msg['email'])}</small>"
    
    messages_html += <<-HTML
    <div style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 15px;">
      <h3>#{CGI.escapeHTML(msg['name'])}</h3>
      #{email_html}
      <p>#{CGI.escapeHTML(msg['message'])}</p>
      <small style="color: #666;">#{timestamp}</small>
    </div>
    HTML
  end
  
  empty_html = messages.empty? ? "<p>还没有留言,快来抢沙发吧!</p>" : ""
  
  <<-HTML
  <!DOCTYPE html>
  <html>
  <head>
    <title>访客留言簿</title>
    <meta charset="UTF-8">
    <style>
      body { font-family: Arial, sans-serif; margin: 40px; max-width: 800px; }
    </style>
  </head>
  <body>
    <h1>访客留言簿</h1>
    #{success_html}
    <p><a href="#{ENV['SCRIPT_NAME']}?action=form">我要留言</a></p>
    
    #{empty_html}
    #{messages_html}
  </body>
  </html>
  HTML
end

简单的搜索程序

ruby
# 简单的文件搜索CGI程序
#!/usr/bin/env ruby

require 'cgi'
require 'find'

cgi = CGI.new

# 配置搜索目录(注意安全)
SEARCH_DIRECTORIES = ['./documents', './public']

def search_files(keyword, directories)
  results = []
  
  directories.each do |dir|
    next unless File.directory?(dir)
    
    Find.find(dir) do |path|
      if File.file?(path) && File.basename(path).downcase.include?(keyword.downcase)
        results << {
          path: path,
          name: File.basename(path),
          size: File.size(path),
          modified: File.mtime(path)
        }
      end
    end
  end
  
  results.sort_by { |r| r[:modified] }.reverse
end

def format_file_size(size)
  units = ['B', 'KB', 'MB', 'GB']
  unit_index = 0
  size_float = size.to_f
  
  while size_float >= 1024 && unit_index < units.length - 1
    size_float /= 1024
    unit_index += 1
  end
  
  "#{size_float.round(2)} #{units[unit_index]}"
end

# 处理搜索请求
keyword = cgi['q'].to_s.strip
action = cgi['action'].to_s

if action == 'search' && !keyword.empty?
  results = search_files(keyword, SEARCH_DIRECTORIES)
  content = show_search_results(keyword, results)
else
  content = show_search_form
end

cgi.out('type' => 'text/html') do
  "<!DOCTYPE html>
  <html>
  <head>
    <title>文件搜索</title>
    <meta charset=\"UTF-8\">
    <style>
      body { font-family: Arial, sans-serif; margin: 40px; max-width: 1000px; }
      .search-form { margin-bottom: 30px; }
      .search-input { padding: 10px; width: 300px; border: 1px solid #ddd; border-radius: 4px; }
      .search-button { padding: 10px 20px; background-color: #007cba; color: white; border: none; border-radius: 4px; cursor: pointer; }
      .search-button:hover { background-color: #005a87; }
      .results { margin-top: 20px; }
      .result-item { border: 1px solid #eee; border-radius: 5px; padding: 15px; margin-bottom: 10px; }
      .result-name { font-size: 18px; font-weight: bold; color: #007cba; }
      .result-path { color: #666; font-size: 14px; margin: 5px 0; }
      .result-meta { color: #999; font-size: 12px; }
    </style>
  </head>
  <body>
    <h1>文件搜索</h1>
    #{content}
  </body>
  </html>"
end

def show_search_form
  <<-HTML
  <div class="search-form">
    <form method="GET" action="#{ENV['SCRIPT_NAME']}">
      <input type="hidden" name="action" value="search">
      <input type="text" name="q" class="search-input" placeholder="输入搜索关键词..." required>
      <button type="submit" class="search-button">搜索</button>
    </form>
  </div>
  <p>搜索目录: #{SEARCH_DIRECTORIES.join(', ')}</p>
  HTML
end

def show_search_results(keyword, results)
  results_html = ""
  
  if results.empty?
    results_html = "<p>没有找到包含 \"#{CGI.escapeHTML(keyword)}\" 的文件。</p>"
  else
    results_html = "<p>找到 #{results.length} 个结果:</p>"
    results.each do |result|
      modified_time = result[:modified].strftime("%Y-%m-%d %H:%M")
      file_size = format_file_size(result[:size])
      
      results_html += <<-HTML
      <div class="result-item">
        <div class="result-name">#{CGI.escapeHTML(result[:name])}</div>
        <div class="result-path">路径: #{CGI.escapeHTML(result[:path])}</div>
        <div class="result-meta">大小: #{file_size} | 修改时间: #{modified_time}</div>
      </div>
      HTML
    end
  end
  
  <<-HTML
  <div class="search-form">
    <form method="GET" action="#{ENV['SCRIPT_NAME']}">
      <input type="hidden" name="action" value="search">
      <input type="text" name="q" class="search-input" value="#{CGI.escapeHTML(keyword)}" placeholder="输入搜索关键词..." required>
      <button type="submit" class="search-button">搜索</button>
    </form>
  </div>
  
  <div class="results">
    #{results_html}
  </div>
  HTML
end

🔧 CGI配置和部署

服务器配置

ruby
# Apache配置示例 (.htaccess 或 httpd.conf)
=begin
# 启用CGI执行
Options +ExecCGI
AddHandler cgi-script .rb

# 或者指定特定目录为CGI目录
ScriptAlias /cgi-bin/ /var/www/cgi-bin/
<Directory "/var/www/cgi-bin">
    Options +ExecCGI
    AddHandler cgi-script .rb
    Require all granted
</Directory>
=end

# Nginx配置示例
=begin
location ~ \.rb$ {
    gzip off;
    root /var/www/cgi-bin;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param REQUEST_METHOD $request_method;
    fastcgi_param CONTENT_TYPE $content_type;
    fastcgi_param CONTENT_LENGTH $content_length;
    fastcgi_param GATEWAY_INTERFACE CGI/1.1;
    fastcgi_param SERVER_SOFTWARE nginx;
    fastcgi_param SCRIPT_NAME $fastcgi_script_name;
    fastcgi_param REQUEST_URI $request_uri;
    fastcgi_param SERVER_PROTOCOL $server_protocol;
    fastcgi_param SERVER_ADDR $server_addr;
    fastcgi_param SERVER_PORT $server_port;
    fastcgi_param SERVER_NAME $server_name;
    fastcgi_param REMOTE_ADDR $remote_addr;
    fastcgi_param REMOTE_PORT $remote_port;
    fastcgi_param HTTPS $https if_not_empty;
    include fastcgi_params;
}
=end

错误处理和日志

ruby
# CGI错误处理和日志记录
#!/usr/bin/env ruby

require 'cgi'
require 'logger'

# 创建日志记录器
logger = Logger.new('cgi_app.log')
logger.level = Logger::INFO

begin
  cgi = CGI.new
  
  # 记录请求信息
  logger.info("CGI请求 - 方法: #{cgi.request_method}, 脚本: #{ENV['SCRIPT_NAME']}")
  
  # 应用逻辑
  name = cgi['name'] || '访客'
  response = "<html><body><h1>Hello, #{CGI.escapeHTML(name)}!</h1></body></html>"
  
  # 输出响应
  cgi.out do
    response
  end
  
  logger.info("CGI响应成功")
  
rescue CGI::Session::CookieStore::CookieOverflow => e
  logger.error("Cookie溢出错误: #{e.message}")
  # 输出错误响应
  CGI.new.out('status' => '500', 'type' => 'text/html') do
    "<html><body><h1>服务器错误</h1><p>Cookie数据过大</p></body></html>"
  end
  
rescue => e
  logger.error("CGI错误: #{e.class} - #{e.message}")
  logger.error("回溯: #{e.backtrace.join("\n")}")
  
  # 输出错误响应
  CGI.new.out('status' => '500', 'type' => 'text/html') do
    "<html><body><h1>服务器内部错误</h1><p>请稍后再试</p></body></html>"
  end
end

性能优化

ruby
# CGI性能优化示例
#!/usr/bin/env ruby

require 'cgi'
require 'benchmark'

cgi = CGI.new

# 使用缓存减少重复计算
def cached_result(key, ttl = 300)  # 5分钟缓存
  cache_file = "cache/#{key}.cache"
  cache_time_file = "cache/#{key}.time"
  
  # 检查缓存是否存在且未过期
  if File.exist?(cache_file) && File.exist?(cache_time_file)
    cache_time = File.read(cache_time_file).to_i
    if Time.now.to_i - cache_time < ttl
      return File.read(cache_file)
    end
  end
  
  # 生成新结果
  result = yield
  
  # 保存到缓存
  FileUtils.mkdir_p('cache') unless File.directory?('cache')
  File.write(cache_file, result)
  File.write(cache_time_file, Time.now.to_i)
  
  result
end

# 测量执行时间
execution_time = Benchmark.realtime do
  # 应用逻辑
  expensive_operation_result = cached_result("expensive_op_#{Time.now.strftime('%Y%m%d')}") do
    # 模拟耗时操作
    sleep(0.1)  # 模拟数据库查询或复杂计算
    "计算结果: #{Time.now}"
  end
  
  @response = "<html><body>
    <h1>CGI性能优化示例</h1>
    <p>#{expensive_operation_result}</p>
    <p>页面生成时间: #{"%.4f" % execution_time} 秒</p>
  </body></html>"
end

cgi.out('type' => 'text/html') do
  @response
end

🛡️ CGI安全最佳实践

1. 输入验证和过滤

ruby
# CGI输入验证和安全处理
#!/usr/bin/env ruby

require 'cgi'
require 'digest'

class CGIInputValidator
  # 验证和清理字符串输入
  def self.sanitize_string(input, max_length = 1000)
    return nil if input.nil?
    
    # 转换为字符串并去除首尾空白
    clean_input = input.to_s.strip
    
    # 检查长度
    return nil if clean_input.length > max_length
    
    # 移除潜在危险字符
    clean_input.gsub(/[<>'"&]/, '')
  end
  
  # 验证邮箱格式
  def self.valid_email?(email)
    return false if email.nil? || email.length > 255
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
  
  # 验证数字范围
  def self.valid_number?(number, min = nil, max = nil)
    return false unless number.is_a?(Numeric)
    return false if min && number < min
    return false if max && number > max
    true
  end
  
  # 验证文件名安全性
  def self.safe_filename?(filename)
    return false if filename.nil? || filename.empty?
    return false if filename.include?('/') || filename.include?('\\')
    return false if filename.start_with?('.')  # 隐藏文件
    true
  end
  
  # 防止跨站脚本攻击(XSS)
  def self.escape_html(text)
    CGI.escapeHTML(text.to_s)
  end
end

# 使用输入验证器的CGI程序
cgi = CGI.new

# 安全地处理输入
name = CGIInputValidator.sanitize_string(cgi['name'])
email = cgi['email']
age = cgi['age'].to_i

# 验证输入
errors = []

if name.nil? || name.empty?
  errors << "姓名不能为空"
elsif name.length < 2
  errors << "姓名至少需要2个字符"
end

if email && !email.empty? && !CGIInputValidator.valid_email?(email)
  errors << "邮箱格式不正确"
end

if age != 0 && !CGIInputValidator.valid_number?(age, 1, 150)
  errors << "年龄必须在1-150之间"
end

if errors.empty?
  response = "<h1>输入验证成功</h1>
              <p>姓名: #{CGIInputValidator.escape_html(name)}</p>
              <p>邮箱: #{CGIInputValidator.escape_html(email || '未提供')}</p>
              <p>年龄: #{age > 0 ? age : '未提供'}</p>"
else
  response = "<h1>输入验证失败</h1>
              <ul>"
  errors.each { |error| response += "<li>#{CGIInputValidator.escape_html(error)}</li>" }
  response += "</ul>"
end

cgi.out do
  "<html>
  <head><title>输入验证示例</title></head>
  <body>
    #{response}
  </body>
  </html>"
end

2. 文件上传安全

ruby
# 安全的文件上传处理
#!/usr/bin/env ruby

require 'cgi'
require 'digest'
require 'filemagic'  # 需要安装filemagic gem

class SecureFileUpload
  ALLOWED_EXTENSIONS = %w[jpg jpeg png gif pdf txt doc docx].freeze
  MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB
  UPLOAD_DIR = 'uploads'
  
  def self.handle_upload(cgi)
    return error_response("没有文件上传") unless cgi.params['file'] && cgi.params['file'][0]
    
    uploaded_file = cgi.params['file'][0]
    filename = uploaded_file.filename
    file_data = uploaded_file.read
    
    # 验证文件
    validation_result = validate_file(filename, file_data)
    return validation_result unless validation_result == true
    
    # 生成安全的文件名
    safe_filename = generate_safe_filename(filename)
    
    # 保存文件
    save_file(safe_filename, file_data)
    
    success_response(safe_filename, file_data.length)
  rescue => e
    error_response("文件上传失败: #{e.message}")
  end
  
  private
  
  def self.validate_file(filename, file_data)
    return "没有选择文件" if filename.nil? || filename.empty?
    return "文件名不安全" unless CGIInputValidator.safe_filename?(filename)
    return "文件过大" if file_data.length > MAX_FILE_SIZE
    
    # 检查文件扩展名
    extension = File.extname(filename).downcase[1..-1]
    return "不允许的文件类型" unless ALLOWED_EXTENSIONS.include?(extension)
    
    # 检查文件内容类型(使用filemagic)
    begin
      fm = FileMagic.new(FileMagic::MAGIC_MIME)
      mime_type = fm.file_from_buffer(file_data)
      fm.close
      
      allowed_mimes = case extension
                      when 'jpg', 'jpeg'
                        ['image/jpeg']
                      when 'png'
                        ['image/png']
                      when 'gif'
                        ['image/gif']
                      when 'pdf'
                        ['application/pdf']
                      when 'txt'
                        ['text/plain']
                      when 'doc'
                        ['application/msword']
                      when 'docx'
                        ['application/vnd.openxmlformats-officedocument.wordprocessingml.document']
                      end
      
      return "文件类型与扩展名不匹配" unless allowed_mimes&.include?(mime_type.split(';')[0])
    rescue
      # 如果无法检测MIME类型,跳过检查
    end
    
    true
  end
  
  def self.generate_safe_filename(original_filename)
    # 生成唯一的安全文件名
    extension = File.extname(original_filename)
    timestamp = Time.now.to_i
    random_string = Digest::SHA256.hexdigest(rand.to_s)[0, 8]
    "#{timestamp}_#{random_string}#{extension}"
  end
  
  def self.save_file(filename, file_data)
    # 确保上传目录存在
    FileUtils.mkdir_p(UPLOAD_DIR) unless File.directory?(UPLOAD_DIR)
    
    # 保存文件
    File.open(File.join(UPLOAD_DIR, filename), 'wb') do |f|
      f.write(file_data)
    end
  end
  
  def self.success_response(filename, size)
    "<h1>文件上传成功</h1>
     <p>文件名: #{CGI.escapeHTML(filename)}</p>
     <p>文件大小: #{size} 字节</p>
     <p><a href=\"#{ENV['SCRIPT_NAME']}\">继续上传</a></p>"
  end
  
  def self.error_response(message)
    "<h1>文件上传失败</h1>
     <p style=\"color: red;\">#{CGI.escapeHTML(message)}</p>
     <p><a href=\"#{ENV['SCRIPT_NAME']}\">返回</a></p>"
  end
end

# 使用安全文件上传
cgi = CGI.new

if cgi.request_method == 'POST'
  response = SecureFileUpload.handle_upload(cgi)
else
  response = show_upload_form
end

cgi.out do
  "<html>
  <head><title>安全文件上传</title></head>
  <body>
    <h1>安全文件上传示例</h1>
    #{response}
  </body>
  </html>"
end

def show_upload_form
  <<-HTML
  <form method="POST" action="#{ENV['SCRIPT_NAME']}" enctype="multipart/form-data">
    <p>
      <label>选择文件 (最大5MB): 
        <input type="file" name="file" accept="image/*,.pdf,.txt,.doc,.docx">
      </label>
    </p>
    <p>
      <input type="submit" value="上传">
    </p>
  </form>
  
  <h2>支持的文件类型:</h2>
  <ul>
    <li>图片: JPG, PNG, GIF</li>
    <li>文档: PDF, TXT, DOC, DOCX</li>
  </ul>
  HTML
end

3. 会话管理

ruby
# CGI会话管理
#!/usr/bin/env ruby

require 'cgi'
require 'securerandom'
require 'json'

class CGISession
  SESSION_DIR = 'sessions'
  SESSION_TIMEOUT = 30 * 60  # 30分钟
  
  def initialize(cgi)
    @cgi = cgi
    @session_id = nil
    @session_data = {}
    
    load_session
  end
  
  def [](key)
    @session_data[key]
  end
  
  def []=(key, value)
    @session_data[key] = value
  end
  
  def delete(key)
    @session_data.delete(key)
  end
  
  def save
    return unless @session_id
    
    # 确保会话目录存在
    FileUtils.mkdir_p(SESSION_DIR) unless File.directory?(SESSION_DIR)
    
    # 添加时间戳
    @session_data['_timestamp'] = Time.now.to_i
    
    # 保存会话数据
    File.write(session_file_path, JSON.generate(@session_data))
    
    # 设置Cookie
    cookie = "#{@session_id}; path=/; HttpOnly"
    cookie += "; Secure" if ENV['HTTPS'] == 'on'
    puts "Set-Cookie: session_id=#{cookie}\n"
  end
  
  def destroy
    return unless @session_id
    
    # 删除会话文件
    File.delete(session_file_path) if File.exist?(session_file_path)
    
    # 清除Cookie
    puts "Set-Cookie: session_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT\n"
    
    @session_id = nil
    @session_data = {}
  end
  
  def logged_in?
    !@session_data['user_id'].nil?
  end
  
  private
  
  def load_session
    # 从Cookie获取会话ID
    cookie_header = ENV['HTTP_COOKIE']
    if cookie_header
      cookies = CGI::Cookie.parse(cookie_header)
      @session_id = cookies['session_id']&.first
    end
    
    # 如果没有会话ID,创建新的
    unless @session_id
      @session_id = SecureRandom.urlsafe_base64(32)
      return
    end
    
    # 加载会话数据
    if File.exist?(session_file_path)
      begin
        data = JSON.parse(File.read(session_file_path))
        
        # 检查会话是否过期
        if data['_timestamp'] && (Time.now.to_i - data['_timestamp']) < SESSION_TIMEOUT
          @session_data = data
        else
          # 会话过期,创建新的
          @session_id = SecureRandom.urlsafe_base64(32)
          @session_data = {}
        end
      rescue
        @session_id = SecureRandom.urlsafe_base64(32)
        @session_data = {}
      end
    end
  end
  
  def session_file_path
    File.join(SESSION_DIR, "#{@session_id}.json")
  end
end

# 使用会话的CGI程序
cgi = CGI.new
session = CGISession.new(cgi)

# 处理登录/登出
if cgi.request_method == 'POST'
  action = cgi['action']
  
  case action
  when 'login'
    username = cgi['username']
    password = cgi['password']
    
    # 简单的认证(实际应用中应该更安全)
    if username == 'admin' && password == 'password'
      session['user_id'] = 1
      session['username'] = username
      session.save
      redirect_to_index
    else
      @error = "用户名或密码错误"
    end
  when 'logout'
    session.destroy
    redirect_to_index
  end
end

# 显示页面
if session.logged_in?
  content = show_logged_in_page(session)
else
  content = show_login_form(@error)
end

cgi.out do
  "<html>
  <head><title>会话管理示例</title></head>
  <body>
    #{content}
  </body>
  </html>"
end

def redirect_to_index
  puts "Status: 302 Found"
  puts "Location: #{ENV['SCRIPT_NAME']}"
  puts ""
  exit
end

def show_login_form(error = nil)
  error_html = error ? "<p style=\"color: red;\">#{CGI.escapeHTML(error)}</p>" : ""
  
  <<-HTML
  <h1>用户登录</h1>
  #{error_html}
  <form method="POST" action="#{ENV['SCRIPT_NAME']}">
    <input type="hidden" name="action" value="login">
    <p>
      <label>用户名: <input type="text" name="username" required></label>
    </p>
    <p>
      <label>密码: <input type="password" name="password" required></label>
    </p>
    <p>
      <input type="submit" value="登录">
    </p>
  </form>
  HTML
end

def show_logged_in_page(session)
  <<-HTML
  <h1>欢迎, #{CGI.escapeHTML(session['username'])}!</h1>
  <p>您已成功登录。</p>
  <form method="POST" action="#{ENV['SCRIPT_NAME']}">
    <input type="hidden" name="action" value="logout">
    <input type="submit" value="登出">
  </form>
  HTML
end

📚 下一步学习

掌握了Ruby CGI编程后,建议继续学习:

继续您的Ruby学习之旅吧!

本站内容仅供学习和研究使用。