Search code examples
webservercrystal-lang

A WEBrick like server with Crystal


Is it possible to create a simple web server with Crystal that can serve HTML, CSS and JS pages?

My current code is:

require "http/server"
Port = 8080
Mime = "text/html"

server = HTTP::Server.new([HTTP::ErrorHandler.new, HTTP::LogHandler.new]) do |context|
    req = context.request
    if req.method == "GET"
        filename = File.join(Dir.current, "index.html")
        context.response.content_type = Mime
        context.response.content_length = File.size(filename)
        File.open(filename) { |file| IO.copy(file, context.response) }
        next
    end
    context.response.content_type = Mime

end

puts "\e[1;33mStarted Listening on Port #{Port}\e[0m"
server.listen(Port)

When I run compile and run the program, it initializes the server, but there are a couple of problems:

  1. In the Firefox browser's "Inspect Element" console, I see:
The stylesheet http://127.0.0.1:8080/styling.css was not loaded because its MIME type, "text/html", is not "text/css". 127.0.0.1:8080

The script from “http://127.0.0.1:8080/javascript.js” was loaded even though its MIME type (“text/html”) is not a valid JavaScript MIME type. 127.0.0.1:8080

SyntaxError: expected expression, got '<' javascript.js:1

The server just shows only the content of index.html.

The codes HTML, CSS and JS is perfectly valid when I run using WEBrick or load up the index.html directly to the browser.

  1. My server isn't accessible from any other devices on the local network.

Solution

  • Many thanks to @Johannes Müller which solved my problem. Here, I am sharing the code for what I wanted exactly.

    Code:

    #!/usr/bin/env crystal
    require "http/server"
    
    # Get the Address
    ADDR = (ARGV.find { |x| x.split(".").size == 4 } || "0.0.0.0").tap { |x| ARGV.delete(x) }
            .split(".").map { |x| x.to_i { 0 } }.join(".")
    
    # Get the Port
    PORT = ARGV.find { |x| x.to_i { 0 } > 0 }.tap { |x| ARGV.delete(x) }.to_s.to_i { 8080 }
    
    # Get the path
    d = Dir.current
    dir = ARGV[0] rescue d
    path = Dir.exists?(dir) ? dir : Dir.exists?(File.join(d, dir)) ? File.join(d, dir) : d
    listing = !!Dir.children(path).find { |x| x == "index.html" }
    actual_path = listing ? File.join(path, "index.html") : path
    
    server = HTTP::Server.new([
            HTTP::ErrorHandler.new,
            HTTP::LogHandler.new,
            HTTP::StaticFileHandler.new(path, directory_listing: !listing)
        ]) do |context|
            context.response.content_type = "text/html"
            File.open(actual_path) { |file| IO.copy(file, context.response) }
    end
    
    puts "\e[1;33m:: Starting Sharing \e[38;5;75m#{actual_path}\e[1;31m on \e[38;5;226mhttp://#{ADDR}:#{PORT}\e[0m"
    server.listen(::ADDR, ::PORT)
    

    This code looks for a "index.html" file to the provided path (default Dir.current), if found, it shares the index.html file to the IP address (default 0.0.0.0) and port (default 8080) provided, else it just shares the current directory contents.

    Running:

    crystal code.cr /tmp/ 5020 127.0.0.1
    

    The options can be shuffled. For example:

    crystal code.cr 5020 /tmp/ 127.0.0.1
    

    Or

    crystal code.cr 5020 127.0.0.1 /tmp
    

    This will start the server and share the /tmp direcotory. If the index.html file is found inside the /tmp/ directory, the requested browser will display the index.html content, or it will work similar to an FTP (although it's not).

    Compiling and Running:

    crystal build code.cr
    ./code [options]