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>"
endCGI环境变量
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
endPOST请求处理
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
endJSON和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>"
end2. 文件上传安全
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
end3. 会话管理
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 发送邮件 - SMTP - 掌握邮件发送功能
- Ruby Socket 编程 - 了解网络编程
- Ruby Web服务 - 学习Web服务开发
- Ruby 多线程 - 掌握并发编程
继续您的Ruby学习之旅吧!