above 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - checksums.yaml.gz.sig +0 -0
 - data/CHANGELOG.md +6 -2
 - data/README.md +19 -8
 - data/lib/above/certs.rb +70 -0
 - data/lib/above/cli.rb +56 -6
 - data/lib/above/config.rb +93 -0
 - data/lib/above/middleware/block.rb +36 -0
 - data/lib/above/middleware/cache.rb +49 -0
 - data/lib/above/middleware/log.rb +73 -0
 - data/lib/above/middleware/redirect.rb +40 -0
 - data/lib/above/middleware/static.rb +96 -0
 - data/lib/above/mime.rb +24 -0
 - data/lib/above/server.rb +154 -0
 - data/lib/above/status.rb +43 -0
 - data/lib/above/utils.rb +25 -0
 - data/lib/above/version.rb +1 -1
 - data/lib/above.rb +3 -0
 - data.tar.gz.sig +0 -0
 - metadata +60 -6
 - metadata.gz.sig +2 -4
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: f7e51e2b89987edc6d43589b7f0cd54c1ac4f5a7fd7e220c563ec468c85f8ea0
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: bb2b4bb89b3a3c23d4e2ba91990c65eae13ce516c69600ce166d7c122a4f1bba
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 98f9509eccf23022d3d3d05e070c627e175008e4b2adb6951cc822d204e44b0eb459adbfe4753197fd79d1bdb7d0dece715b4ea1e4a808543862540cebf03eff
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 242ae0538a1c634ddfd2ab6ece8ff9b8061813da931f896be0195424b6f88668204433630c51dd4e5d210993821ef4491122293861841a81e99a6e0da9198913
         
     | 
    
        checksums.yaml.gz.sig
    CHANGED
    
    | 
         Binary file 
     | 
    
        data/CHANGELOG.md
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,20 +1,31 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # Above
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
      
 3 
     | 
    
         
            +
            An async Gemini Protocol server with support for multi-tenancy. Above supports middleware, with default middleware for blocking, naive in-memory caching, logging, redirects, & a static file server included. Also included are utilities for certificate creation & DNS-based authentication (DANE).
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
       5 
     | 
    
         
            -
            ##  
     | 
| 
      
 5 
     | 
    
         
            +
            ## Status
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
      
 7 
     | 
    
         
            +
            3 - Alpha in Python terms.
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
      
 9 
     | 
    
         
            +
            (Those Python statuses are 1 - Planning, 2 - Pre-alpha, 3 - Alpha, 4 - Beta, 5 - Production/Stable, 6 - Mature, 7 - Inactive.)
         
     | 
| 
       10 
10 
     | 
    
         | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
      
 11 
     | 
    
         
            +
            Currently not handled:
         
     | 
| 
      
 12 
     | 
    
         
            +
            - user input or client certificates
         
     | 
| 
      
 13 
     | 
    
         
            +
            - proxy requests to other domains or protocols
         
     | 
| 
      
 14 
     | 
    
         
            +
            - anything not quite in the standard. Maybe...
         
     | 
| 
       12 
15 
     | 
    
         | 
| 
       13 
     | 
    
         
            -
            ##  
     | 
| 
      
 16 
     | 
    
         
            +
            ## Why
         
     | 
| 
       14 
17 
     | 
    
         | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
      
 18 
     | 
    
         
            +
            I've read that the heyday of the Gemini Protocol's passed. I don't know, even if it has passed it's still an interesting take on fixing some of what ails the web. If only to delve into the depths of TCP & TLS, & to appreciate again the wonders of HTTP.
         
     | 
| 
       16 
19 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
      
 20 
     | 
    
         
            +
            None of the other Gemini servers I looked at offered quite the same mix of features as Above in quite the same way. So here we are.
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
            ## Installation
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            `gem install above`
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            ## Usage
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
            ...
         
     | 
| 
       18 
29 
     | 
    
         | 
| 
       19 
30 
     | 
    
         
             
            ## Development
         
     | 
| 
       20 
31 
     | 
    
         | 
    
        data/lib/above/certs.rb
    ADDED
    
    | 
         @@ -0,0 +1,70 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "openssl"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Create and load certificates
         
     | 
| 
      
 7 
     | 
    
         
            +
              class Certs
         
     | 
| 
      
 8 
     | 
    
         
            +
                def dane(domain_arr:, dir:)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  file = File.read("#{File.join(dir, domain_arr.first)}.crt")
         
     | 
| 
      
 10 
     | 
    
         
            +
                  cert = OpenSSL::X509::Certificate.new(file)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  checksum = OpenSSL::Digest::SHA512.new(cert.to_der)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  puts "For DANE create:\n\n"
         
     | 
| 
      
 13 
     | 
    
         
            +
                  domain_arr.each do |domain|
         
     | 
| 
      
 14 
     | 
    
         
            +
                    puts "DNS record: _1965._tcp.#{domain}\n"
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
                  puts <<~HELP
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    Each with the text: TLSA 3 1 2 #{checksum}
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    312 meaning the certificate's self signed, check the public cert, against this SHA-512 hash
         
     | 
| 
      
 21 
     | 
    
         
            +
                    See: https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities
         
     | 
| 
      
 22 
     | 
    
         
            +
                  HELP
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                # Create key & cert for domain in directory, with a one year duration
         
     | 
| 
      
 26 
     | 
    
         
            +
                def create(domain_arr:, dir:, duration: 60 * 60 * 24 * 365)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  # RSA is new & length, not generate & algo
         
     | 
| 
      
 28 
     | 
    
         
            +
                  key = OpenSSL::PKey::EC.generate("secp384r1")
         
     | 
| 
      
 29 
     | 
    
         
            +
                  name = OpenSSL::X509::Name.parse("/CN=#{domain_arr.first}")
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  time = Time.now
         
     | 
| 
      
 32 
     | 
    
         
            +
                  cert = OpenSSL::X509::Certificate.new
         
     | 
| 
      
 33 
     | 
    
         
            +
                  cert.version = 2
         
     | 
| 
      
 34 
     | 
    
         
            +
                  cert.serial = 0
         
     | 
| 
      
 35 
     | 
    
         
            +
                  cert.not_before = time
         
     | 
| 
      
 36 
     | 
    
         
            +
                  cert.not_after = time + duration
         
     | 
| 
      
 37 
     | 
    
         
            +
                  # RSA is key.public_key not key - why are they different!?
         
     | 
| 
      
 38 
     | 
    
         
            +
                  cert.public_key = key
         
     | 
| 
      
 39 
     | 
    
         
            +
                  cert.subject = name
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  extension_factory = OpenSSL::X509::ExtensionFactory.new(nil, cert)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  cert.add_extension \
         
     | 
| 
      
 43 
     | 
    
         
            +
                    extension_factory.create_extension("basicConstraints", "CA:FALSE", true)
         
     | 
| 
      
 44 
     | 
    
         
            +
                  cert.add_extension \
         
     | 
| 
      
 45 
     | 
    
         
            +
                    extension_factory.create_extension(
         
     | 
| 
      
 46 
     | 
    
         
            +
                      "keyUsage", "nonRepudiation, digitalSignature, keyEncipherment"
         
     | 
| 
      
 47 
     | 
    
         
            +
                    )
         
     | 
| 
      
 48 
     | 
    
         
            +
                  dns_str = ""
         
     | 
| 
      
 49 
     | 
    
         
            +
                  domain_arr.each do |domain|
         
     | 
| 
      
 50 
     | 
    
         
            +
                    dns_str += "DNS:#{domain},"
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                  dns_str.chomp!(",")
         
     | 
| 
      
 53 
     | 
    
         
            +
                  cert.add_extension \
         
     | 
| 
      
 54 
     | 
    
         
            +
                    extension_factory.create_extension(
         
     | 
| 
      
 55 
     | 
    
         
            +
                      "subjectAltName", dns_str
         
     | 
| 
      
 56 
     | 
    
         
            +
                    )
         
     | 
| 
      
 57 
     | 
    
         
            +
                  cert.add_extension \
         
     | 
| 
      
 58 
     | 
    
         
            +
                    extension_factory.create_extension(
         
     | 
| 
      
 59 
     | 
    
         
            +
                      "extendedKeyUsage", "serverAuth, clientAuth"
         
     | 
| 
      
 60 
     | 
    
         
            +
                    )
         
     | 
| 
      
 61 
     | 
    
         
            +
                  cert.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash")
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                  cert.issuer = name
         
     | 
| 
      
 64 
     | 
    
         
            +
                  cert.sign key, OpenSSL::Digest.new("SHA512")
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                  File.write(File.join(dir, "#{domain_arr.first}.crt"), cert.to_pem)
         
     | 
| 
      
 67 
     | 
    
         
            +
                  File.write(File.join(dir, "#{domain_arr.first}.key"), key.to_pem)
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
              end
         
     | 
| 
      
 70 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/above/cli.rb
    CHANGED
    
    | 
         @@ -1,22 +1,62 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require "optparse"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require_relative "certs"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative "config"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "server"
         
     | 
| 
       4 
7 
     | 
    
         | 
| 
       5 
8 
     | 
    
         
             
            module Above
         
     | 
| 
       6 
9 
     | 
    
         
             
              # CLI handled by optparse
         
     | 
| 
       7 
10 
     | 
    
         
             
              class CLI
         
     | 
| 
       8 
11 
     | 
    
         
             
                def start(*_args)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @options = {}
         
     | 
| 
       9 
13 
     | 
    
         
             
                  OptionParser.new do |opts|
         
     | 
| 
       10 
14 
     | 
    
         
             
                    opts.banner = <<~BANNER
         
     | 
| 
       11 
15 
     | 
    
         
             
                      Above
         
     | 
| 
       12 
     | 
    
         
            -
                       
     | 
| 
      
 16 
     | 
    
         
            +
                      A Gemini Protocol server
         
     | 
| 
       13 
17 
     | 
    
         | 
| 
       14 
18 
     | 
    
         
             
                      Commands:
         
     | 
| 
       15 
19 
     | 
    
         
             
                    BANNER
         
     | 
| 
      
 20 
     | 
    
         
            +
                    certs(opts)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    dir(opts)
         
     | 
| 
      
 22 
     | 
    
         
            +
                    server(opts)
         
     | 
| 
       16 
23 
     | 
    
         
             
                    help(opts)
         
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
       18 
25 
     | 
    
         
             
                    version(opts)
         
     | 
| 
       19 
26 
     | 
    
         
             
                  end.parse!
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  create_cert
         
     | 
| 
      
 29 
     | 
    
         
            +
                  start_server
         
     | 
| 
      
 30 
     | 
    
         
            +
                rescue OptionParser::InvalidOption
         
     | 
| 
      
 31 
     | 
    
         
            +
                  puts "Invalid option"
         
     | 
| 
      
 32 
     | 
    
         
            +
                  # Don't have opts here to show help
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def certs(opts)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  opts.on("-c", "--cert DOMAINS", "Make a certificate for DOMAINS (comma seperated list)") do |opt|
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @options[:domain] = opt
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def create_cert
         
     | 
| 
      
 42 
     | 
    
         
            +
                  if @options.key?(:domain)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    if !@options.key?(:dir)
         
     | 
| 
      
 44 
     | 
    
         
            +
                      puts "Directory argument missing"
         
     | 
| 
      
 45 
     | 
    
         
            +
                      exit
         
     | 
| 
      
 46 
     | 
    
         
            +
                    end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    domain_arr = @options[:domain].split(",")
         
     | 
| 
      
 49 
     | 
    
         
            +
                    dir = @options[:dir]
         
     | 
| 
      
 50 
     | 
    
         
            +
                    cert = Above::Certs.new
         
     | 
| 
      
 51 
     | 
    
         
            +
                    cert.create(domain_arr:, dir:)
         
     | 
| 
      
 52 
     | 
    
         
            +
                    cert.dane(domain_arr:, dir:)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  end
         
     | 
| 
      
 54 
     | 
    
         
            +
                end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                def dir(opts)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  opts.on("-d", "--dir DIRECTORY", "Make a certificate in, or look for config in DIRECTORY") do |opt|
         
     | 
| 
      
 58 
     | 
    
         
            +
                    @options[:dir] = opt
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
       20 
60 
     | 
    
         
             
                end
         
     | 
| 
       21 
61 
     | 
    
         | 
| 
       22 
62 
     | 
    
         
             
                def help(opts)
         
     | 
| 
         @@ -26,10 +66,20 @@ module Above 
     | 
|
| 
       26 
66 
     | 
    
         
             
                  end
         
     | 
| 
       27 
67 
     | 
    
         
             
                end
         
     | 
| 
       28 
68 
     | 
    
         | 
| 
       29 
     | 
    
         
            -
                def  
     | 
| 
       30 
     | 
    
         
            -
                  opts.on("- 
     | 
| 
       31 
     | 
    
         
            -
                     
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
      
 69 
     | 
    
         
            +
                def server(opts)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  opts.on("-s", "--start", "Start the server, if a config directory isn't found one will be created") do |opt|
         
     | 
| 
      
 71 
     | 
    
         
            +
                    @options[:server] = opt
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                def start_server
         
     | 
| 
      
 76 
     | 
    
         
            +
                  return unless @options[:server]
         
     | 
| 
      
 77 
     | 
    
         
            +
                  dir = @options[:dir]
         
     | 
| 
      
 78 
     | 
    
         
            +
                  config = Above::Config.new.read_config(dir:)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  config.each_pair do |server_name, server_hash|
         
     | 
| 
      
 80 
     | 
    
         
            +
                    puts "Starting #{server_name}"
         
     | 
| 
      
 81 
     | 
    
         
            +
                    server = Above::Server.new(config: server_hash)
         
     | 
| 
      
 82 
     | 
    
         
            +
                    server.start
         
     | 
| 
       33 
83 
     | 
    
         
             
                  end
         
     | 
| 
       34 
84 
     | 
    
         
             
                end
         
     | 
| 
       35 
85 
     | 
    
         | 
    
        data/lib/above/config.rb
    ADDED
    
    | 
         @@ -0,0 +1,93 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "fileutils"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "yaml"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require_relative "utils"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 9 
     | 
    
         
            +
              # Persist & use configuration
         
     | 
| 
      
 10 
     | 
    
         
            +
              class Config
         
     | 
| 
      
 11 
     | 
    
         
            +
                attr_reader :domains
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def self.config_dir
         
     | 
| 
      
 14 
     | 
    
         
            +
                  File.join(Utils.home_dir, ".config/above")
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                def self.domains_dir
         
     | 
| 
      
 18 
     | 
    
         
            +
                  File.join(Config.config_dir, "domains")
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def self.exemplar
         
     | 
| 
      
 22 
     | 
    
         
            +
                  {
         
     | 
| 
      
 23 
     | 
    
         
            +
                    "server" => {
         
     | 
| 
      
 24 
     | 
    
         
            +
                      "domain" => "localhost",
         
     | 
| 
      
 25 
     | 
    
         
            +
                      "max_fibers" => 20,
         
     | 
| 
      
 26 
     | 
    
         
            +
                      "port" => 1965,
         
     | 
| 
      
 27 
     | 
    
         
            +
                      "tls_cert" => "/home/above/localhost.crt",
         
     | 
| 
      
 28 
     | 
    
         
            +
                      "tls_key" => "/home/above/localhost.key"
         
     | 
| 
      
 29 
     | 
    
         
            +
                    },
         
     | 
| 
      
 30 
     | 
    
         
            +
                    "middleware" => {
         
     | 
| 
      
 31 
     | 
    
         
            +
                      "Above::MiddleWare::Block" => {
         
     | 
| 
      
 32 
     | 
    
         
            +
                        "blocklist" => [
         
     | 
| 
      
 33 
     | 
    
         
            +
                          "192.168.2.0/24"
         
     | 
| 
      
 34 
     | 
    
         
            +
                        ]
         
     | 
| 
      
 35 
     | 
    
         
            +
                      },
         
     | 
| 
      
 36 
     | 
    
         
            +
                      "Above::MiddleWare::Cache" => {
         
     | 
| 
      
 37 
     | 
    
         
            +
                        "max_size" => 12
         
     | 
| 
      
 38 
     | 
    
         
            +
                      },
         
     | 
| 
      
 39 
     | 
    
         
            +
                      "Above::MiddleWare::Log" => {
         
     | 
| 
      
 40 
     | 
    
         
            +
                        "log_file" => "localhost.log"
         
     | 
| 
      
 41 
     | 
    
         
            +
                      },
         
     | 
| 
      
 42 
     | 
    
         
            +
                      "Above::MiddleWare::Redirect" => {
         
     | 
| 
      
 43 
     | 
    
         
            +
                        "redirect_permanent" => {
         
     | 
| 
      
 44 
     | 
    
         
            +
                          "/from" => "/to"
         
     | 
| 
      
 45 
     | 
    
         
            +
                        },
         
     | 
| 
      
 46 
     | 
    
         
            +
                        "redirect_temporary" => {
         
     | 
| 
      
 47 
     | 
    
         
            +
                          "/from" => "/to"
         
     | 
| 
      
 48 
     | 
    
         
            +
                        }
         
     | 
| 
      
 49 
     | 
    
         
            +
                      },
         
     | 
| 
      
 50 
     | 
    
         
            +
                      "Above::MiddleWare::Static" => {
         
     | 
| 
      
 51 
     | 
    
         
            +
                        "docroot" => "/home/above/www",
         
     | 
| 
      
 52 
     | 
    
         
            +
                        "index" => "index.gmi",
         
     | 
| 
      
 53 
     | 
    
         
            +
                        "lang" => "en"
         
     | 
| 
      
 54 
     | 
    
         
            +
                      }
         
     | 
| 
      
 55 
     | 
    
         
            +
                    }
         
     | 
| 
      
 56 
     | 
    
         
            +
                  }
         
     | 
| 
      
 57 
     | 
    
         
            +
                end
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 60 
     | 
    
         
            +
                  init_dir
         
     | 
| 
      
 61 
     | 
    
         
            +
                end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                def init_dir
         
     | 
| 
      
 64 
     | 
    
         
            +
                  if !File.exist?(Config.domains_dir)
         
     | 
| 
      
 65 
     | 
    
         
            +
                    FileUtils.mkdir_p(Config.domains_dir)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    File.write(File.join(Config.config_dir, "example.yml"), Config.exemplar.to_yaml)
         
     | 
| 
      
 67 
     | 
    
         
            +
                    puts "Configuration directory created"
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
                  if Dir.empty?(Config.domains_dir)
         
     | 
| 
      
 70 
     | 
    
         
            +
                    puts "No configuration files found"
         
     | 
| 
      
 71 
     | 
    
         
            +
                    exit
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
                end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                def read_config(dir: nil)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  dir ||= Config.domains_dir
         
     | 
| 
      
 77 
     | 
    
         
            +
                  files = Dir[File.join(dir, "**/*.yml")]
         
     | 
| 
      
 78 
     | 
    
         
            +
                  domains = {}
         
     | 
| 
      
 79 
     | 
    
         
            +
                  files.each do |file|
         
     | 
| 
      
 80 
     | 
    
         
            +
                    hash = YAML.safe_load_file(file, permitted_classes: [Hash])
         
     | 
| 
      
 81 
     | 
    
         
            +
                    Utils.check_keys(expected_keys: Config.exemplar.keys, file:, hash:)
         
     | 
| 
      
 82 
     | 
    
         
            +
                    Utils.check_keys(expected_keys: Config.exemplar["server"].keys, file:, hash: hash["server"])
         
     | 
| 
      
 83 
     | 
    
         
            +
                    # Middleware config to be checked in each piece of middleware
         
     | 
| 
      
 84 
     | 
    
         
            +
                    domains[hash["server"]["domain"]] = hash
         
     | 
| 
      
 85 
     | 
    
         
            +
                  end
         
     | 
| 
      
 86 
     | 
    
         
            +
                  if domains.size == 0
         
     | 
| 
      
 87 
     | 
    
         
            +
                    puts "No configuration files found"
         
     | 
| 
      
 88 
     | 
    
         
            +
                    exit
         
     | 
| 
      
 89 
     | 
    
         
            +
                  end
         
     | 
| 
      
 90 
     | 
    
         
            +
                  domains
         
     | 
| 
      
 91 
     | 
    
         
            +
                end
         
     | 
| 
      
 92 
     | 
    
         
            +
              end
         
     | 
| 
      
 93 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "ipaddr"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 6 
     | 
    
         
            +
              module MiddleWare
         
     | 
| 
      
 7 
     | 
    
         
            +
                # Middleware to block requests based on IP addresses, also supports IPv6 & /CIDR notation
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Block
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize(app:)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @app = app # who you gonna call
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  def call(env:)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # TODO - maybe return - 41? 44? - chosen via config
         
     | 
| 
      
 15 
     | 
    
         
            +
                    requester = env[:socket].io.remote_address.ip_address
         
     | 
| 
      
 16 
     | 
    
         
            +
                    blocklist = env[:config]["Above::MiddleWare::Block"]["blocklist"]
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    blocklist&.each do |bad_address_range|
         
     | 
| 
      
 19 
     | 
    
         
            +
                      bad_addr = IPAddr.new(bad_address_range)
         
     | 
| 
      
 20 
     | 
    
         
            +
                      if bad_addr.include?(requester)
         
     | 
| 
      
 21 
     | 
    
         
            +
                        # return here, otherwise go on to reply in the usual way
         
     | 
| 
      
 22 
     | 
    
         
            +
                        return [0, "", []]
         
     | 
| 
      
 23 
     | 
    
         
            +
                      end
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    @app&.call(env:)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  rescue IPAddr::InvalidAddressError
         
     | 
| 
      
 28 
     | 
    
         
            +
                    puts "Error - Invalid address in blocklist"
         
     | 
| 
      
 29 
     | 
    
         
            +
                    [42, Status::CODE[42], []]
         
     | 
| 
      
 30 
     | 
    
         
            +
                  end
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
            end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            # b = Above::MiddleWare::Block.new(app: nil)
         
     | 
| 
      
 36 
     | 
    
         
            +
            # b.call(env: nil)
         
     | 
| 
         @@ -0,0 +1,49 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "time"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 6 
     | 
    
         
            +
              module MiddleWare
         
     | 
| 
      
 7 
     | 
    
         
            +
                # Middleware for a naive cache that stores the last max_size worth of results in memory
         
     | 
| 
      
 8 
     | 
    
         
            +
                class Cache
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize(app:)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @app = app # who you gonna call
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @cache = {}
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @max_size = nil
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def call(env:)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # env is {socket:, request:, config:, valid:}
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    # can't cache an invalid url
         
     | 
| 
      
 19 
     | 
    
         
            +
                    return @app&.call(env:) unless env[:valid]
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                    if @max_size.nil?
         
     | 
| 
      
 22 
     | 
    
         
            +
                      config = env[:config]["Above::MiddleWare::Cache"]
         
     | 
| 
      
 23 
     | 
    
         
            +
                      @max_size = config["max_size"]
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    request = env[:request].to_s.chomp("/")
         
     | 
| 
      
 27 
     | 
    
         
            +
                    if @cache.key?(request)
         
     | 
| 
      
 28 
     | 
    
         
            +
                      # use the cached result if we have it
         
     | 
| 
      
 29 
     | 
    
         
            +
                      @cache[request][:result]
         
     | 
| 
      
 30 
     | 
    
         
            +
                    else
         
     | 
| 
      
 31 
     | 
    
         
            +
                      # otherwise go on to find & cache the result
         
     | 
| 
      
 32 
     | 
    
         
            +
                      result = @app.call(env:) unless @app.nil?
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                      # only keep the most recent @max_size worth of results
         
     | 
| 
      
 35 
     | 
    
         
            +
                      if @cache.size >= @max_size
         
     | 
| 
      
 36 
     | 
    
         
            +
                        oldest = @cache.min_by { |key, val| val[:time] }[0]
         
     | 
| 
      
 37 
     | 
    
         
            +
                        @cache.delete(oldest)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      end
         
     | 
| 
      
 39 
     | 
    
         
            +
                      @cache[request] = {
         
     | 
| 
      
 40 
     | 
    
         
            +
                        result: result,
         
     | 
| 
      
 41 
     | 
    
         
            +
                        time: Time.now
         
     | 
| 
      
 42 
     | 
    
         
            +
                      }
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                      result
         
     | 
| 
      
 45 
     | 
    
         
            +
                    end
         
     | 
| 
      
 46 
     | 
    
         
            +
                  end
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
              end
         
     | 
| 
      
 49 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,73 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative "../status"
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 6 
     | 
    
         
            +
              module MiddleWare
         
     | 
| 
      
 7 
     | 
    
         
            +
                # Middleware to log replies
         
     | 
| 
      
 8 
     | 
    
         
            +
                # Apache common log format https://httpd.apache.org/docs/2.4/logs.html#common
         
     | 
| 
      
 9 
     | 
    
         
            +
                # via Puma https://github.com/puma/puma/blob/master/lib/puma/commonlogger.rb
         
     | 
| 
      
 10 
     | 
    
         
            +
                #
         
     | 
| 
      
 11 
     | 
    
         
            +
                # ip - - [time] "request" status size
         
     | 
| 
      
 12 
     | 
    
         
            +
                # eg
         
     | 
| 
      
 13 
     | 
    
         
            +
                # ::1 - - [24/Aug/2024:11:20:46 +1000] "gemini://localhost/" 20 143
         
     | 
| 
      
 14 
     | 
    
         
            +
                class Log
         
     | 
| 
      
 15 
     | 
    
         
            +
                  FORMAT = %(%s - - [%s] "%s" %d %d\n)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  LOG_TIME_FORMAT = "%d/%b/%Y:%H:%M:%S %z"
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  def initialize(app:)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    @app = app # who you gonna call
         
     | 
| 
      
 20 
     | 
    
         
            +
                    @logger = nil
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  def call(env:)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    if @logger.nil?
         
     | 
| 
      
 25 
     | 
    
         
            +
                      config = env[:config]["Above::MiddleWare::Log"]
         
     | 
| 
      
 26 
     | 
    
         
            +
                      # 3 one meg ("mebibyte") files rotated - TODO: chosen via config
         
     | 
| 
      
 27 
     | 
    
         
            +
                      @logger = Logger.new(File.expand_path(config["log_file"]), 3, 1048576)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    status, header, body_arr = @app.call(env:) unless @app.nil?
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                    # TODO: streaming, multipart
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    body = if body_arr.nil? || !body_arr.is_a?(Array) || body_arr.first.nil?
         
     | 
| 
      
 35 
     | 
    
         
            +
                      ""
         
     | 
| 
      
 36 
     | 
    
         
            +
                    else
         
     | 
| 
      
 37 
     | 
    
         
            +
                      body_arr.first
         
     | 
| 
      
 38 
     | 
    
         
            +
                    end
         
     | 
| 
      
 39 
     | 
    
         
            +
                    log(ip: env[:socket].io.remote_address.ip_address,
         
     | 
| 
      
 40 
     | 
    
         
            +
                      request: env[:request],
         
     | 
| 
      
 41 
     | 
    
         
            +
                      status:,
         
     | 
| 
      
 42 
     | 
    
         
            +
                      size: body.size) # body size, not response size
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    [status, header, [body]]
         
     | 
| 
      
 45 
     | 
    
         
            +
                  rescue
         
     | 
| 
      
 46 
     | 
    
         
            +
                    # catch-all for "CGI" errors in the middleware
         
     | 
| 
      
 47 
     | 
    
         
            +
                    [42, Status::CODE[42], []]
         
     | 
| 
      
 48 
     | 
    
         
            +
                  end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                  def log(ip:, request:, status:, size:)
         
     | 
| 
      
 51 
     | 
    
         
            +
                    began_at = Time.now
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                    msg = FORMAT % [ip,
         
     | 
| 
      
 54 
     | 
    
         
            +
                      began_at.strftime(LOG_TIME_FORMAT),
         
     | 
| 
      
 55 
     | 
    
         
            +
                      request.to_s,
         
     | 
| 
      
 56 
     | 
    
         
            +
                      status,
         
     | 
| 
      
 57 
     | 
    
         
            +
                      size]
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                    write(msg)
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  def write(msg)
         
     | 
| 
      
 63 
     | 
    
         
            +
                    # Standard library logger doesn't support write but it supports << which actually
         
     | 
| 
      
 64 
     | 
    
         
            +
                    # calls to write on the log device without formatting
         
     | 
| 
      
 65 
     | 
    
         
            +
                    if @logger.respond_to?(:write)
         
     | 
| 
      
 66 
     | 
    
         
            +
                      @logger.write(msg)
         
     | 
| 
      
 67 
     | 
    
         
            +
                    else
         
     | 
| 
      
 68 
     | 
    
         
            +
                      @logger << msg
         
     | 
| 
      
 69 
     | 
    
         
            +
                    end
         
     | 
| 
      
 70 
     | 
    
         
            +
                  end
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
      
 73 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,40 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 4 
     | 
    
         
            +
              module MiddleWare
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Middleware for redirects
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Redirect
         
     | 
| 
      
 7 
     | 
    
         
            +
                  def initialize(app:)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    @app = app # who you gonna call
         
     | 
| 
      
 9 
     | 
    
         
            +
                  end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def call(env:)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # env is {socket:, request:, config:, valid:}
         
     | 
| 
      
 13 
     | 
    
         
            +
                    # redirects from local paths to local or remote ones
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # config format is eg:
         
     | 
| 
      
 15 
     | 
    
         
            +
                    # "Above::MiddleWare::Redirect":
         
     | 
| 
      
 16 
     | 
    
         
            +
                    #   redirect_permanent:
         
     | 
| 
      
 17 
     | 
    
         
            +
                    #     "/bad.txt": "/good.txt"
         
     | 
| 
      
 18 
     | 
    
         
            +
                    #     "/sad": "gemini://other.site/" etc
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                    # Can't redirect from an invalid url
         
     | 
| 
      
 21 
     | 
    
         
            +
                    return @app&.call(env:) unless env[:valid]
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                    request = env[:request].path
         
     | 
| 
      
 24 
     | 
    
         
            +
                    config = env[:config]["Above::MiddleWare::Redirect"]
         
     | 
| 
      
 25 
     | 
    
         
            +
                    permanent_redirects = config["redirect_permanent"]
         
     | 
| 
      
 26 
     | 
    
         
            +
                    temporary_redirects = config["redirect_temporary"]
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    if permanent_redirects.key?(request)
         
     | 
| 
      
 29 
     | 
    
         
            +
                      return [30, permanent_redirects[request], []]
         
     | 
| 
      
 30 
     | 
    
         
            +
                    elsif temporary_redirects.key?(request)
         
     | 
| 
      
 31 
     | 
    
         
            +
                      return [30, temporary_redirects[request], []]
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    # Otherise pass it on
         
     | 
| 
      
 35 
     | 
    
         
            +
                    status, header, body_arr = @app.call(env:)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    [status, header, body_arr]
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,96 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 4 
     | 
    
         
            +
              module MiddleWare
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Middleware to serve static files
         
     | 
| 
      
 6 
     | 
    
         
            +
                class Static
         
     | 
| 
      
 7 
     | 
    
         
            +
                  def initialize(app:)
         
     | 
| 
      
 8 
     | 
    
         
            +
                    @app = app # who you gonna call
         
     | 
| 
      
 9 
     | 
    
         
            +
                  end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def call(env:)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # env is {socket:, request:, config:, server:, valid:}
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                    status, header, body_arr = @app.call(env:) unless @app.nil? # usually nothing to call from here
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    # If redirect or cache fired we wouldn't get to here in the default arrangemnet. Usually wouldn't have any
         
     | 
| 
      
 17 
     | 
    
         
            +
                    # other middleware to call after this one. If indeed we don't, or if that didn't create a response, try to get the file requested
         
     | 
| 
      
 18 
     | 
    
         
            +
                    if status.nil?
         
     | 
| 
      
 19 
     | 
    
         
            +
                      response(env:)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    else
         
     | 
| 
      
 21 
     | 
    
         
            +
                      [status, header, body_arr]
         
     | 
| 
      
 22 
     | 
    
         
            +
                    end
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def format_response(maybe_incomplete_triplet:, env:)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    lang = env[:config]["Above::MiddleWare::Static"]["lang"]
         
     | 
| 
      
 27 
     | 
    
         
            +
                    status, header, content_arr = maybe_incomplete_triplet
         
     | 
| 
      
 28 
     | 
    
         
            +
                    if status < 20 || status > 59
         
     | 
| 
      
 29 
     | 
    
         
            +
                      # TODO: input requests (from some other middleware?) not currently handled
         
     | 
| 
      
 30 
     | 
    
         
            +
                      [42, Status::CODE[42], []]
         
     | 
| 
      
 31 
     | 
    
         
            +
                    elsif status > 39
         
     | 
| 
      
 32 
     | 
    
         
            +
                      # Ensure errors have some header text
         
     | 
| 
      
 33 
     | 
    
         
            +
                      header = Status::CODE[status] if header.nil?
         
     | 
| 
      
 34 
     | 
    
         
            +
                      [status, header, content_arr]
         
     | 
| 
      
 35 
     | 
    
         
            +
                    elsif header.start_with?("text/")
         
     | 
| 
      
 36 
     | 
    
         
            +
                      # TODO: multipart, streaming etc
         
     | 
| 
      
 37 
     | 
    
         
            +
                      # header here is the mime_type, from #read_file
         
     | 
| 
      
 38 
     | 
    
         
            +
                      content = content_arr.first.encode(Encoding.find("UTF-8"), invalid: :replace, undef: :replace, replace: "")
         
     | 
| 
      
 39 
     | 
    
         
            +
                      [status, "#{header}; charset=utf-8; lang=#{lang}; size=#{content.length}", [content]]
         
     | 
| 
      
 40 
     | 
    
         
            +
                    else
         
     | 
| 
      
 41 
     | 
    
         
            +
                      [status, "#{header}; size=#{content_arr.first.length}", content_arr]
         
     | 
| 
      
 42 
     | 
    
         
            +
                    end
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  def read_file(request:, env:, again: false)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    config = env[:config]["Above::MiddleWare::Static"]
         
     | 
| 
      
 47 
     | 
    
         
            +
                    path = File.join(File.expand_path(config["docroot"]), request.path)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    mime_type = Mime.type(path:) # TODO - maybe replace marcel, this reads files twice...
         
     | 
| 
      
 49 
     | 
    
         
            +
                    content = File.read(path)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    # Maybe TODO: multi part, streaming etc
         
     | 
| 
      
 51 
     | 
    
         
            +
                    # nb header is incomplete at this point
         
     | 
| 
      
 52 
     | 
    
         
            +
                    [20, mime_type, [content]]
         
     | 
| 
      
 53 
     | 
    
         
            +
                  rescue Errno::EISDIR
         
     | 
| 
      
 54 
     | 
    
         
            +
                    if !again
         
     | 
| 
      
 55 
     | 
    
         
            +
                      try_index(request:, env:, index: config["index"])
         
     | 
| 
      
 56 
     | 
    
         
            +
                    else
         
     | 
| 
      
 57 
     | 
    
         
            +
                      [51, Status::CODE[51], []]
         
     | 
| 
      
 58 
     | 
    
         
            +
                    end
         
     | 
| 
      
 59 
     | 
    
         
            +
                  rescue Errno::ENOENT
         
     | 
| 
      
 60 
     | 
    
         
            +
                    [51, Status::CODE[51], []]
         
     | 
| 
      
 61 
     | 
    
         
            +
                  end
         
     | 
| 
      
 62 
     | 
    
         
            +
             
     | 
| 
      
 63 
     | 
    
         
            +
                  def response(env:)
         
     | 
| 
      
 64 
     | 
    
         
            +
                    host = env[:server]["domain"]
         
     | 
| 
      
 65 
     | 
    
         
            +
                    request = env[:request]
         
     | 
| 
      
 66 
     | 
    
         
            +
                    response = if !env[:valid]
         
     | 
| 
      
 67 
     | 
    
         
            +
                      # Bad request caught earlier
         
     | 
| 
      
 68 
     | 
    
         
            +
                      [59, Status::CODE[59], []]
         
     | 
| 
      
 69 
     | 
    
         
            +
                    elsif request.to_s.length > 1024
         
     | 
| 
      
 70 
     | 
    
         
            +
                      # Request longer than maximum length
         
     | 
| 
      
 71 
     | 
    
         
            +
                      [59, Status::CODE[59], []]
         
     | 
| 
      
 72 
     | 
    
         
            +
                    elsif request.scheme != "gemini" || request.host != host
         
     | 
| 
      
 73 
     | 
    
         
            +
                      # Currently - refuse proxy requests for different protocols or servers, maybe TODO
         
     | 
| 
      
 74 
     | 
    
         
            +
                      [53, Status::CODE[53], []]
         
     | 
| 
      
 75 
     | 
    
         
            +
                    else
         
     | 
| 
      
 76 
     | 
    
         
            +
                      maybe_incomplete_triplet = read_file(request:, env:)
         
     | 
| 
      
 77 
     | 
    
         
            +
                      format_response(maybe_incomplete_triplet:, env:)
         
     | 
| 
      
 78 
     | 
    
         
            +
                    end
         
     | 
| 
      
 79 
     | 
    
         
            +
                  rescue NoMethodError
         
     | 
| 
      
 80 
     | 
    
         
            +
                    # eg trying to encode nil content
         
     | 
| 
      
 81 
     | 
    
         
            +
                    response = [40, Status::CODE[40], []]
         
     | 
| 
      
 82 
     | 
    
         
            +
                  rescue
         
     | 
| 
      
 83 
     | 
    
         
            +
                    # Bad uri, no scheme etc that got this far
         
     | 
| 
      
 84 
     | 
    
         
            +
                    response = [59, Status::CODE[59], []]
         
     | 
| 
      
 85 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 86 
     | 
    
         
            +
                    response
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                  def try_index(request:, env:, index:)
         
     | 
| 
      
 90 
     | 
    
         
            +
                    new_request = request.dup
         
     | 
| 
      
 91 
     | 
    
         
            +
                    new_request.path = File.join(request.path, index)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    read_file(request: new_request, env:, again: true)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/above/mime.rb
    ADDED
    
    | 
         @@ -0,0 +1,24 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "marcel"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "pathname"
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 7 
     | 
    
         
            +
              # Mime types
         
     | 
| 
      
 8 
     | 
    
         
            +
              module Mime
         
     | 
| 
      
 9 
     | 
    
         
            +
                # Mime type based on extension for Gemtext files &
         
     | 
| 
      
 10 
     | 
    
         
            +
                # Marcel for everything else
         
     | 
| 
      
 11 
     | 
    
         
            +
                # throws Errno::ENOENT on missing non gemtext files
         
     | 
| 
      
 12 
     | 
    
         
            +
                # Marcel::MimeType.extend "text/gemini", extensions: %w[.gmi .gemini] doesn't seem to work?
         
     | 
| 
      
 13 
     | 
    
         
            +
                def type(path:)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  if %w[.gmi .gemini].include? File.extname(path)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    "text/gemini"
         
     | 
| 
      
 16 
     | 
    
         
            +
                  else
         
     | 
| 
      
 17 
     | 
    
         
            +
                    name = File.basename(path)
         
     | 
| 
      
 18 
     | 
    
         
            +
                    Marcel::MimeType.for(Pathname.new(path), name: name)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                module_function :type
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/above/server.rb
    ADDED
    
    | 
         @@ -0,0 +1,154 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require "async"
         
     | 
| 
      
 4 
     | 
    
         
            +
            require "async/barrier"
         
     | 
| 
      
 5 
     | 
    
         
            +
            require "async/semaphore"
         
     | 
| 
      
 6 
     | 
    
         
            +
            require "logger"
         
     | 
| 
      
 7 
     | 
    
         
            +
            require "openssl"
         
     | 
| 
      
 8 
     | 
    
         
            +
            require "uri"
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            require_relative "config"
         
     | 
| 
      
 11 
     | 
    
         
            +
            require_relative "middleware/block"
         
     | 
| 
      
 12 
     | 
    
         
            +
            require_relative "middleware/cache"
         
     | 
| 
      
 13 
     | 
    
         
            +
            require_relative "middleware/log"
         
     | 
| 
      
 14 
     | 
    
         
            +
            require_relative "middleware/redirect"
         
     | 
| 
      
 15 
     | 
    
         
            +
            require_relative "middleware/static"
         
     | 
| 
      
 16 
     | 
    
         
            +
            require_relative "mime"
         
     | 
| 
      
 17 
     | 
    
         
            +
            require_relative "status"
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 20 
     | 
    
         
            +
              # Answer TCP requests
         
     | 
| 
      
 21 
     | 
    
         
            +
              class Server
         
     | 
| 
      
 22 
     | 
    
         
            +
                def initialize(config:, middleware: [MiddleWare::Log,
         
     | 
| 
      
 23 
     | 
    
         
            +
                  MiddleWare::Block,
         
     | 
| 
      
 24 
     | 
    
         
            +
                  MiddleWare::Cache,
         
     | 
| 
      
 25 
     | 
    
         
            +
                  MiddleWare::Redirect,
         
     | 
| 
      
 26 
     | 
    
         
            +
                  MiddleWare::Static])
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @config = config
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @server_config = config["server"]
         
     | 
| 
      
 29 
     | 
    
         
            +
                  middleware_init(middleware:)
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def listener(tls:, limit:)
         
     | 
| 
      
 33 
     | 
    
         
            +
                  limit.async do
         
     | 
| 
      
 34 
     | 
    
         
            +
                    socket = tls.accept
         
     | 
| 
      
 35 
     | 
    
         
            +
                    # env is {socket:, request:, config:, valid:}
         
     | 
| 
      
 36 
     | 
    
         
            +
                    env = request(socket:)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    response = middleware(env:)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    reply(env:, response:)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  rescue => error
         
     | 
| 
      
 40 
     | 
    
         
            +
                    puts error.message
         
     | 
| 
      
 41 
     | 
    
         
            +
                    # may be unable to reply if error is with socket
         
     | 
| 
      
 42 
     | 
    
         
            +
                    # TODO: logging at a level above the logging middleware
         
     | 
| 
      
 43 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 44 
     | 
    
         
            +
                    socket&.close
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                # Instantiate each middleware class with an app vairable pointing to the
         
     | 
| 
      
 49 
     | 
    
         
            +
                # next piece of middleware to call
         
     | 
| 
      
 50 
     | 
    
         
            +
                def middleware_init(middleware:)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @middleware = []
         
     | 
| 
      
 52 
     | 
    
         
            +
                  previous = nil
         
     | 
| 
      
 53 
     | 
    
         
            +
                  middleware.reverse_each do |mid|
         
     | 
| 
      
 54 
     | 
    
         
            +
                    instance = mid.new(app: previous)
         
     | 
| 
      
 55 
     | 
    
         
            +
                    previous = instance
         
     | 
| 
      
 56 
     | 
    
         
            +
                    @middleware.push(instance)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                  @middleware.reverse!
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def middleware(env:)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  # Each piece of middleware will call the next, the result here is
         
     | 
| 
      
 63 
     | 
    
         
            +
                  # [status, header, [body]], just like in rack
         
     | 
| 
      
 64 
     | 
    
         
            +
                  @middleware.first.call(env:)
         
     | 
| 
      
 65 
     | 
    
         
            +
                rescue
         
     | 
| 
      
 66 
     | 
    
         
            +
                  # TODO - log this
         
     | 
| 
      
 67 
     | 
    
         
            +
                  [42, Status::CODE[42], []]
         
     | 
| 
      
 68 
     | 
    
         
            +
                end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                def on_quit(barrier:)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  %w[INT TERM].each do |signal|
         
     | 
| 
      
 72 
     | 
    
         
            +
                    Signal.trap(signal) do
         
     | 
| 
      
 73 
     | 
    
         
            +
                      barrier.stop
         
     | 
| 
      
 74 
     | 
    
         
            +
                      puts "\nShutting down"
         
     | 
| 
      
 75 
     | 
    
         
            +
                      exit
         
     | 
| 
      
 76 
     | 
    
         
            +
                    end
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
                end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                def reply(env:, response:)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  # TODO: uncertain if there's meant to be a space before the \r\n
         
     | 
| 
      
 82 
     | 
    
         
            +
                  # mentioned in https://geminiprotocol.net/news/2024_07_30.gmi
         
     | 
| 
      
 83 
     | 
    
         
            +
                  # Maybe TODO: streaming, multipart replies etc
         
     | 
| 
      
 84 
     | 
    
         
            +
                  status, header, body_arr = response
         
     | 
| 
      
 85 
     | 
    
         
            +
                  if status === 0
         
     | 
| 
      
 86 
     | 
    
         
            +
                    # blocked
         
     | 
| 
      
 87 
     | 
    
         
            +
                    env[:socket]&.close
         
     | 
| 
      
 88 
     | 
    
         
            +
                    return
         
     | 
| 
      
 89 
     | 
    
         
            +
                  elsif status.nil?
         
     | 
| 
      
 90 
     | 
    
         
            +
                    status = 40
         
     | 
| 
      
 91 
     | 
    
         
            +
                    header = Status::CODE[40]
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
                  body = if body_arr.nil? || !body_arr.is_a?(Array) || body_arr.first.nil?
         
     | 
| 
      
 94 
     | 
    
         
            +
                    ""
         
     | 
| 
      
 95 
     | 
    
         
            +
                  else
         
     | 
| 
      
 96 
     | 
    
         
            +
                    body_arr.first
         
     | 
| 
      
 97 
     | 
    
         
            +
                  end
         
     | 
| 
      
 98 
     | 
    
         
            +
                  env[:socket].puts "#{status} #{header}\r\n#{body}"
         
     | 
| 
      
 99 
     | 
    
         
            +
                rescue
         
     | 
| 
      
 100 
     | 
    
         
            +
                  env[:socket].puts "40 #{Status::CODE[40]}\r\n"
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                # creates env hash, {socket:, request:, config:, server:, valid:}
         
     | 
| 
      
 104 
     | 
    
         
            +
                def request(socket:)
         
     | 
| 
      
 105 
     | 
    
         
            +
                  config = @config["middleware"]
         
     | 
| 
      
 106 
     | 
    
         
            +
                  str = socket.gets.chomp
         
     | 
| 
      
 107 
     | 
    
         
            +
                  request = URI(str)
         
     | 
| 
      
 108 
     | 
    
         
            +
                  {socket:, request:, config:, server: @server_config, valid: true}
         
     | 
| 
      
 109 
     | 
    
         
            +
                rescue URI::InvalidURIError
         
     | 
| 
      
 110 
     | 
    
         
            +
                  {socket:, request: str[0...1023], config:, server: @server_config, valid: false}
         
     | 
| 
      
 111 
     | 
    
         
            +
                end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                def serve(tls:)
         
     | 
| 
      
 114 
     | 
    
         
            +
                  barrier = Async::Barrier.new
         
     | 
| 
      
 115 
     | 
    
         
            +
                  Sync do
         
     | 
| 
      
 116 
     | 
    
         
            +
                    on_quit(barrier:)
         
     | 
| 
      
 117 
     | 
    
         
            +
                    limit = Async::Semaphore.new(@server_config["max_fibers"], parent: barrier)
         
     | 
| 
      
 118 
     | 
    
         
            +
                    loop do
         
     | 
| 
      
 119 
     | 
    
         
            +
                      listener(tls:, limit:)
         
     | 
| 
      
 120 
     | 
    
         
            +
                    end
         
     | 
| 
      
 121 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 122 
     | 
    
         
            +
                    barrier.stop
         
     | 
| 
      
 123 
     | 
    
         
            +
                  end
         
     | 
| 
      
 124 
     | 
    
         
            +
                end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                def start
         
     | 
| 
      
 127 
     | 
    
         
            +
                  serve(tls: tls_server)
         
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                def tls_server
         
     | 
| 
      
 131 
     | 
    
         
            +
                  tcp_server = TCPServer.new(@server_config["domain"], @server_config["port"])
         
     | 
| 
      
 132 
     | 
    
         
            +
                  ssl_ctx = OpenSSL::SSL::SSLContext.new.tap do |ctx|
         
     | 
| 
      
 133 
     | 
    
         
            +
                    ctx.key = OpenSSL::PKey::EC.new(
         
     | 
| 
      
 134 
     | 
    
         
            +
                      File.read(
         
     | 
| 
      
 135 
     | 
    
         
            +
                        File.expand_path(
         
     | 
| 
      
 136 
     | 
    
         
            +
                          @server_config["tls_key"]
         
     | 
| 
      
 137 
     | 
    
         
            +
                        )
         
     | 
| 
      
 138 
     | 
    
         
            +
                      )
         
     | 
| 
      
 139 
     | 
    
         
            +
                    )
         
     | 
| 
      
 140 
     | 
    
         
            +
                    ctx.cert = OpenSSL::X509::Certificate.new(
         
     | 
| 
      
 141 
     | 
    
         
            +
                      File.read(
         
     | 
| 
      
 142 
     | 
    
         
            +
                        File.expand_path(
         
     | 
| 
      
 143 
     | 
    
         
            +
                          @server_config["tls_cert"]
         
     | 
| 
      
 144 
     | 
    
         
            +
                        )
         
     | 
| 
      
 145 
     | 
    
         
            +
                      )
         
     | 
| 
      
 146 
     | 
    
         
            +
                    )
         
     | 
| 
      
 147 
     | 
    
         
            +
                  end
         
     | 
| 
      
 148 
     | 
    
         
            +
                  OpenSSL::SSL::SSLServer.new(tcp_server, ssl_ctx)
         
     | 
| 
      
 149 
     | 
    
         
            +
                rescue => error
         
     | 
| 
      
 150 
     | 
    
         
            +
                  puts "Certificate/Key loading errors - #{error.message}"
         
     | 
| 
      
 151 
     | 
    
         
            +
                  exit
         
     | 
| 
      
 152 
     | 
    
         
            +
                end
         
     | 
| 
      
 153 
     | 
    
         
            +
              end
         
     | 
| 
      
 154 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/above/status.rb
    ADDED
    
    | 
         @@ -0,0 +1,43 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Gemini status codes, includes non standard codes 0-9, otherwise
         
     | 
| 
      
 5 
     | 
    
         
            +
              # from https://geminiprotocol.net/docs/protocol-specification.gmi
         
     | 
| 
      
 6 
     | 
    
         
            +
              module Status
         
     | 
| 
      
 7 
     | 
    
         
            +
                CODE = {
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # 0-9 Non standard codes
         
     | 
| 
      
 9 
     | 
    
         
            +
                  0 => "Address blocked",
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  # 10-19 Input expected
         
     | 
| 
      
 12 
     | 
    
         
            +
                  10 => "User input requested",
         
     | 
| 
      
 13 
     | 
    
         
            +
                  11 => "Sensitive user input requested",
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  # 20-29 Success
         
     | 
| 
      
 16 
     | 
    
         
            +
                  20 => "Success",
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  # 30-39 Redirection
         
     | 
| 
      
 19 
     | 
    
         
            +
                  30 => "Temporary redirection",
         
     | 
| 
      
 20 
     | 
    
         
            +
                  31 => "Permanent redirection",
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  # 40+ descriptions are included in the header
         
     | 
| 
      
 23 
     | 
    
         
            +
                  # 40-49 Temporary failure
         
     | 
| 
      
 24 
     | 
    
         
            +
                  40 => "Temporary failure",
         
     | 
| 
      
 25 
     | 
    
         
            +
                  41 => "Server unavailable",
         
     | 
| 
      
 26 
     | 
    
         
            +
                  42 => "CGI error",
         
     | 
| 
      
 27 
     | 
    
         
            +
                  43 => "Proxy error from/with the proxied server",
         
     | 
| 
      
 28 
     | 
    
         
            +
                  44 => "Slow down",
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  # 50-59 Permanent failure
         
     | 
| 
      
 31 
     | 
    
         
            +
                  50 => "General permanent failure",
         
     | 
| 
      
 32 
     | 
    
         
            +
                  51 => "Not found",
         
     | 
| 
      
 33 
     | 
    
         
            +
                  52 => "Gone permanently",
         
     | 
| 
      
 34 
     | 
    
         
            +
                  53 => "Proxy request refused by this server",
         
     | 
| 
      
 35 
     | 
    
         
            +
                  59 => "Bad request",
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  # 60-69 Client certificates
         
     | 
| 
      
 38 
     | 
    
         
            +
                  60 => "Content requires a client certificate",
         
     | 
| 
      
 39 
     | 
    
         
            +
                  61 => "Certificate not authorized",
         
     | 
| 
      
 40 
     | 
    
         
            +
                  62 => "Certificate not valid (unrelated to request)"
         
     | 
| 
      
 41 
     | 
    
         
            +
                }
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/above/utils.rb
    ADDED
    
    | 
         @@ -0,0 +1,25 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Above
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Utility functions
         
     | 
| 
      
 5 
     | 
    
         
            +
              module Utils
         
     | 
| 
      
 6 
     | 
    
         
            +
                def check_keys(expected_keys:, file:, hash:)
         
     | 
| 
      
 7 
     | 
    
         
            +
                  if !expected_keys.all? { |key| hash.key? key }
         
     | 
| 
      
 8 
     | 
    
         
            +
                    puts "Missing keys - #{file}"
         
     | 
| 
      
 9 
     | 
    
         
            +
                    (expected_keys - hash.keys).each { |key| puts key }
         
     | 
| 
      
 10 
     | 
    
         
            +
                    exit
         
     | 
| 
      
 11 
     | 
    
         
            +
                  end
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                def home_dir
         
     | 
| 
      
 15 
     | 
    
         
            +
                  path = if RUBY_PLATFORM.match?(/cygwin | mswin | mingw | bccwin | wince | emx /)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    "%userprofile%"
         
     | 
| 
      
 17 
     | 
    
         
            +
                  else
         
     | 
| 
      
 18 
     | 
    
         
            +
                    "~"
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                  File.expand_path(path)
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                module_function :check_keys, :home_dir
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/above/version.rb
    CHANGED
    
    
    
        data/lib/above.rb
    CHANGED
    
    
    
        data.tar.gz.sig
    CHANGED
    
    | 
         Binary file 
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: above
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.2.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Mike Kreuzer
         
     | 
| 
         @@ -35,11 +35,54 @@ cert_chain: 
     | 
|
| 
       35 
35 
     | 
    
         
             
              YgqTixCwts6cQYIdYNFtJbzKvRNqyviKKxPaum7UWAv6Uy80gxgJ8p+fG81FsxbZ
         
     | 
| 
       36 
36 
     | 
    
         
             
              0ULnXrHlhf/CHs550TxRlXalgxBCImGdHzWjhdJeC1dZP3olgNtjO23ywtw=
         
     | 
| 
       37 
37 
     | 
    
         
             
              -----END CERTIFICATE-----
         
     | 
| 
       38 
     | 
    
         
            -
            date: 2024-08- 
     | 
| 
       39 
     | 
    
         
            -
            dependencies: 
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
      
 38 
     | 
    
         
            +
            date: 2024-08-24 00:00:00.000000000 Z
         
     | 
| 
      
 39 
     | 
    
         
            +
            dependencies:
         
     | 
| 
      
 40 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 41 
     | 
    
         
            +
              name: async
         
     | 
| 
      
 42 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 43 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 44 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 45 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 46 
     | 
    
         
            +
                    version: '2.15'
         
     | 
| 
      
 47 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 48 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 49 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 50 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 51 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 52 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 53 
     | 
    
         
            +
                    version: '2.15'
         
     | 
| 
      
 54 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 55 
     | 
    
         
            +
              name: marcel
         
     | 
| 
      
 56 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 57 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 58 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 59 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 60 
     | 
    
         
            +
                    version: '1.0'
         
     | 
| 
      
 61 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 62 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 63 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 64 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 65 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 66 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 67 
     | 
    
         
            +
                    version: '1.0'
         
     | 
| 
      
 68 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 69 
     | 
    
         
            +
              name: openssl
         
     | 
| 
      
 70 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 71 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 72 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 73 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 74 
     | 
    
         
            +
                    version: '3.2'
         
     | 
| 
      
 75 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 76 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 77 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 78 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 79 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 80 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 81 
     | 
    
         
            +
                    version: '3.2'
         
     | 
| 
      
 82 
     | 
    
         
            +
            description: |
         
     | 
| 
      
 83 
     | 
    
         
            +
              An async Gemini Protocol server with support for multi-tenancy.
         
     | 
| 
       41 
84 
     | 
    
         | 
| 
       42 
     | 
    
         
            -
               
     | 
| 
      
 85 
     | 
    
         
            +
              Above supports middleware, with default middleware for blocking, naive in-memory caching, logging, redirects, & a static file server included. Also included are utilities for certificate creation & DNS-based authentication (DANE).
         
     | 
| 
       43 
86 
     | 
    
         
             
            email:
         
     | 
| 
       44 
87 
     | 
    
         
             
            - mike@mikekreuzer.com
         
     | 
| 
       45 
88 
     | 
    
         
             
            executables:
         
     | 
| 
         @@ -54,7 +97,18 @@ files: 
     | 
|
| 
       54 
97 
     | 
    
         
             
            - bin/console
         
     | 
| 
       55 
98 
     | 
    
         
             
            - bin/setup
         
     | 
| 
       56 
99 
     | 
    
         
             
            - lib/above.rb
         
     | 
| 
      
 100 
     | 
    
         
            +
            - lib/above/certs.rb
         
     | 
| 
       57 
101 
     | 
    
         
             
            - lib/above/cli.rb
         
     | 
| 
      
 102 
     | 
    
         
            +
            - lib/above/config.rb
         
     | 
| 
      
 103 
     | 
    
         
            +
            - lib/above/middleware/block.rb
         
     | 
| 
      
 104 
     | 
    
         
            +
            - lib/above/middleware/cache.rb
         
     | 
| 
      
 105 
     | 
    
         
            +
            - lib/above/middleware/log.rb
         
     | 
| 
      
 106 
     | 
    
         
            +
            - lib/above/middleware/redirect.rb
         
     | 
| 
      
 107 
     | 
    
         
            +
            - lib/above/middleware/static.rb
         
     | 
| 
      
 108 
     | 
    
         
            +
            - lib/above/mime.rb
         
     | 
| 
      
 109 
     | 
    
         
            +
            - lib/above/server.rb
         
     | 
| 
      
 110 
     | 
    
         
            +
            - lib/above/status.rb
         
     | 
| 
      
 111 
     | 
    
         
            +
            - lib/above/utils.rb
         
     | 
| 
       58 
112 
     | 
    
         
             
            - lib/above/version.rb
         
     | 
| 
       59 
113 
     | 
    
         
             
            homepage: https://codeberg.org/kreuzer/kreuzer/above
         
     | 
| 
       60 
114 
     | 
    
         
             
            licenses:
         
     | 
| 
         @@ -82,5 +136,5 @@ requirements: [] 
     | 
|
| 
       82 
136 
     | 
    
         
             
            rubygems_version: 3.5.17
         
     | 
| 
       83 
137 
     | 
    
         
             
            signing_key:
         
     | 
| 
       84 
138 
     | 
    
         
             
            specification_version: 4
         
     | 
| 
       85 
     | 
    
         
            -
            summary: A Gemini Protocol  
     | 
| 
      
 139 
     | 
    
         
            +
            summary: A Gemini Protocol server
         
     | 
| 
       86 
140 
     | 
    
         
             
            test_files: []
         
     | 
    
        metadata.gz.sig
    CHANGED
    
    | 
         @@ -1,4 +1,2 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
            ��hL�T��r?)���g��츗�}N�W�ԙl:��D2��%t�6̯LRo�^�!��w)�U
         
     | 
| 
       4 
     | 
    
         
            -
            b���4xV����G϶g��yIeSw\���q�>���� �
         
     | 
| 
      
 1 
     | 
    
         
            +
            e�we�'歂].8���5�t�1-�9�E�au�7Ʌ䆮f�lf�h��#��V��G���O�����<a���ۄI'߯I�q�vh�Hs;�+��u����p;�H�\1v`�S���@��$�tvn+jw��I��S�����ciEh�W���'�8;x�AD��/��fY=�������.�8 �m�j����)s��.��+�P-ҍ'g`7L��9.�e��P��}�+���9�d�O�5�6ޯ��������UG�'!)�F\q��h��Q���4
         
     | 
| 
      
 2 
     | 
    
         
            +
            @^
         
     |