ADDED Makefile Index: Makefile ================================================================== --- /dev/null +++ Makefile @@ -0,0 +1,5 @@ +fs : src/fs.cr shard.lock + crystal build --no-color src/fs.cr + +shard.lock : shard.yml + shards install ADDED fs.yml.sample Index: fs.yml.sample ================================================================== --- /dev/null +++ fs.yml.sample @@ -0,0 +1,11 @@ +--- +# The following parameters are required. +root: /var/www/html +port: 8083 +host: 127.0.0.1 +# The following parameters are optional. +#sslport: 8453 +#key: /etc/letsencrypt/live/www.example.com/privkey.pem +#cert: /etc/letsencrypt/live/www.example.com/fullchain.pem +log: ./fs.log +loglevel: ERROR ADDED shard.yml Index: shard.yml ================================================================== --- /dev/null +++ shard.yml @@ -0,0 +1,17 @@ +name: fs +version: 0.1.0 + +authors: + - Mark Alexander + +targets: + fs: + main: src/fs.cr + +crystal: '>= 1.10.0' + +license: MIT + +dependencies: + logger: + github: crystal-lang/logger.cr ADDED src/fs.cr Index: src/fs.cr ================================================================== --- /dev/null +++ src/fs.cr @@ -0,0 +1,175 @@ +# fs - Crystal implementation of a simple static file server + +require "uri" +require "http" +require "option_parser" +require "yaml" +require "logger" + +# Class for reading configuration information from fs.yml. + +class Config + # Required settings + getter host : String # 127.0.0.1 for localhost, 0.0.0.0 for all hosts + getter port : Int32 + getter root : String + + # Optional settings + getter sslport : (Int32|Nil) + getter key : (String|Nil) + getter cert : (String|Nil) + getter log : (String|Nil) + getter loglevel : (String|Nil) + + def initialize(config_file : String) + yaml = File.open(config_file) {|file| YAML.parse(file) } + + # host, port, and root are required. + @host = yaml["host"].as_s + @port = yaml["port"].as_i + @root = yaml["root"].as_s + + # sslport, key, and cert are optional. + if yaml["sslport"]? + @sslport = yaml["sslport"].as_i + @key = yaml["key"].as_s + @cert = yaml["cert"].as_s + end + + # log and loglevel are optional. + if yaml["log"]? + @log = yaml["log"].as_s + end + if yaml["loglevel"]? + @loglevel = yaml["loglevel"].as_s + end + end +end + +# Logger class that reads the log level and log filename from fs.yml + +module MyLog + extend self + + @@log = uninitialized Logger + + def configure(config : Config) + levels = { + "DEBUG" => Logger::DEBUG, + "ERROR" => Logger::ERROR, + "FATAL" => Logger::FATAL, + "INFO" => Logger::INFO, + "UNKNOWN" => Logger::UNKNOWN, + "WARN" => Logger::WARN + } + + filename = config.log || "" + loglevel = config.loglevel || "DEBUG" + if filename.size > 0 + file = File.open(filename, "a+") + @@log = Logger.new(file) + else + @@log = Logger.new(STDOUT) + end + @@log.level = levels[loglevel.upcase] + end + + delegate debug, to: @@log + delegate error, to: @@log + delegate fatal, to: @@log + delegate info, to: @@log + delegate unknown, to: @@log + delegate warn, to: @@log + + def close + @log.close + end +end + +# Simple HTTP server for static files. + +class Server + def initialize(config : Config) + @config = config + @server = uninitialized HTTP::Server + end + + def process_request(context : HTTP::Server::Context) + request = context.request + request_path = request.path + method = request.method + root = @config.root + + MyLog.debug "process_request: got path #{request_path}, method #{method}" + + path = File.join(root, request_path) + if Dir.exists?(path) + path = File.join(path, "index.html") + end + if File.exists?(path) + context.response.content_type = `file -b --mime-type #{path}`.strip + File.open(path) { |file| IO.copy(file, context.response) } + else + context.response.respond_with_status(404, "No such file #{request_path}") + end + end + + def start + @server = HTTP::Server.new do |context| + process_request(context) + end + + if @server + address = @server.bind_tcp @config.host, @config.port + puts "Listening on http://#{address}" + + # If SSL is specified, set that up. If you have Apache + # available, it's probably better to NOT let fs handle + # SSL, but let Apache handle it and put fs behind a + # reverse proxy. + if @config.sslport + ssl_context = OpenSSL::SSL::Context::Server.new + ssl_context.certificate_chain = @config.cert || "" + ssl_context.private_key = @config.key || "" + @server.bind_tls "0.0.0.0", @config.sslport || 0, ssl_context + puts "Listening on SSL port #{@config.sslport}" + end + @server.listen + end + end + + def stop + MyLog.debug "Server::stop" + if @server + @server.close + end + end +end + +def doit + banner = <<-BANNER +fs [options] +BANNER + + config_file = "./fs.yml" + + OptionParser.parse do |parser| + parser.banner = banner + parser.on("-c FILENAME", "--config=FILENAME", + "Specifies the config filename") { |name| config_file = name } + end + + # Read config file. + puts "Using config file " + config_file + config = Config.new(config_file); + + # Set up logging. + MyLog.configure(config) + + # Start the server. + server = Server.new(config) + server.start + server.stop +end + +doit