#! /usr/bin/env ruby -W1 # -*- ruby -*- # $Id: https-file-dist,v 1.1 2007/01/05 19:43:20 slumos Exp $ # # Set up an ad hoc HTTPS server for the purpose of distributing a strictly # limited batch of static files. require 'optparse' require 'ostruct' require 'webrick' require 'webrick/httpauth' require 'webrick/https' include WEBrick require 'ruby-debug' Debugger.start class Options PROGRAM_VERSION = 0.1 def self.parse(args) options = OpenStruct.new options.hostname = Utils::getservername options.server = { :ServerSoftware => "#{File.basename $0}/#{PROGRAM_VERSION}", :BindAddress => '0.0.0.0', :Port => 8443, :SSLEnable => true, :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE, :SSLCertName => [ ['C', 'US'], ['O', 'ISRI'], ['CN', "#{options.hostname}"] ] } options.logger = Log.new($stderr) options.username = nil options.password = '' options.realm = options.hostname opts = OptionParser.new do |opts| opts.banner = "usage: #{File.basename $0} [options] files..." opts.separator '' opts.separator 'Network options:' opts.on('-a', '--address SPEC', "Address and port to bind.", " (default=#{options.server[:BindAddress]}:#{options.server[:Port]})") do |spec| address, port = spec.split(':', 2) if address.empty? or address == '*' then options.server[:BindAddress] = nil else options.server[:BindAddress] = address end options.server[:Port] = port end opts.separator '' opts.separator 'Authentication options:' opts.on('-u', '--username USER', 'Require username') {|options.username|} opts.on('-p', '--password PASS', 'Require password') {|options.password|} opts.on('--realm REALM', 'Name of realm to send with Basic Auth req', " (default=#{options.realm})") do |realm| options.realm = realm end opts.separator '' opts.separator 'Common options:' opts.on('-h', '-?', '--help', 'Show this message.') do puts opts exit end opts.on('-v', '--verbose [N]', Numeric, 'Set log level (0(fatal)<=N<=5(debug))') do |level| options.logger = Log.new $stderr, (level || 5) options.server[:Logger] = options.logger end opts.on('-V', '--version', 'Print program version and exit') do puts PROGRAM_VERSION exit end end opts.parse!(args) if args.empty? then options.logger.fatal 'required file argument(s) missing (try -h).' exit 127 end return options end end class Fixnum def pretty_size k = Float(1024) m = Float(1024*k) g = Float(1024*m) if self < k then return self.to_s elsif self < m then return '%.1fK' % (self / k) elsif self < g then return '%.1fM' % (self / m) else return '%.1fG' % (self / g) end end end class HTTPSFileDist < HTTPServlet::AbstractServlet class Error < StandardError; end class Initialization < Error; end def initialize(server, options, args) super(server) @auth_username = options.username @auth_password = options.password @auth_realm = options.realm @files = {} args.each do |filename| s = File.stat(filename.dup.untaint) unless s.file? and s.readable? then raise Error::Initialization, "#{filename}: not a readable file" end basename = File.basename(filename) if @files.member? basename then @logger.warn "overriding filename #{basename} from #{@files[basename][0]} to #{filename}" end @files[basename] = [filename, s.mtime, s.size] end end def do_GET(req, res) if @auth_username then HTTPAuth.basic_auth(req, res, @auth_realm) do |username, password| username == @auth_username and password == @auth_password end end @logger.debug 'got here' basename = File.basename(req.path) if basename == '/' then index(req, res) elsif @files.member? basename then filename = @files[basename][0].dup.untaint res['content-type'] = HTTPUtils.mime_type(filename, @config[:MimeTypes]) res['last-modified'] = @files[basename][1] res['content-length'] = @files[basename][2] res.body = open(filename, 'rb') else raise HTTPStatus::NotFound, "#{basename} not found" end end def index(req, res) res['content-type'] = 'text/html' res.body << <<-END.gsub(/^ */, '')
Name Last Modified Size
END
@files.sort.each do |basename, (filename, mtime, size)|
res.body << "#{basename}" << ' ' * (40 - basename.length)
res.body << mtime.strftime('%Y-%m-%d %H:%M:%S')
res.body << size.pretty_size.rjust(9)
res.body << "\n"
end
res.body << <<-END.strip.gsub(/^ */, '')
END
end
end
options = Options.parse(ARGV)
server = HTTPServer.new options.server
server.mount '/', HTTPSFileDist, options, ARGV
['INT', 'TERM'].each {|sig| trap(sig) { server.shutdown }}
$SAFE = 1
server.start