Class: Above::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/above/server.rb

Overview

Answer TCP requests

Instance Method Summary collapse

Constructor Details

#initialize(config:, middleware: [MiddleWare::Log, MiddleWare::Block, MiddleWare::Cache, MiddleWare::Redirect, MiddleWare::Static]) ⇒ Server

Returns a new instance of Server.



22
23
24
25
26
27
28
29
30
# File 'lib/above/server.rb', line 22

def initialize(config:, middleware: [MiddleWare::Log,
  MiddleWare::Block,
  MiddleWare::Cache,
  MiddleWare::Redirect,
  MiddleWare::Static])
  @config = config
  @server_config = config["server"]
  middleware_init(middleware:)
end

Instance Method Details

#listener(tls:, limit:) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/above/server.rb', line 32

def listener(tls:, limit:)
  limit.async do
    socket = tls.accept
    # env is {socket:, request:, config:, valid:}
    env = request(socket:)
    response = middleware(env:)
    reply(env:, response:)
  rescue => error
    puts error.message
    # may be unable to reply if error is with socket
    # TODO: logging at a level above the logging middleware
  ensure
    socket&.close
  end
end

#middleware(env:) ⇒ Object



61
62
63
64
65
66
67
68
# File 'lib/above/server.rb', line 61

def middleware(env:)
  # Each piece of middleware will call the next, the result here is
  # [status, header, [body]], just like in rack
  @middleware.first.call(env:)
rescue
  # TODO - log this
  [42, Status::CODE[42], []]
end

#middleware_init(middleware:) ⇒ Object

Instantiate each middleware class with an app vairable pointing to the next piece of middleware to call



50
51
52
53
54
55
56
57
58
59
# File 'lib/above/server.rb', line 50

def middleware_init(middleware:)
  @middleware = []
  previous = nil
  middleware.reverse_each do |mid|
    instance = mid.new(app: previous)
    previous = instance
    @middleware.push(instance)
  end
  @middleware.reverse!
end

#on_quit(barrier:) ⇒ Object



70
71
72
73
74
75
76
77
78
# File 'lib/above/server.rb', line 70

def on_quit(barrier:)
  %w[INT TERM].each do |signal|
    Signal.trap(signal) do
      barrier.stop
      puts "\nShutting down"
      exit
    end
  end
end

#reply(env:, response:) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/above/server.rb', line 80

def reply(env:, response:)
  # TODO: uncertain if there's meant to be a space before the \r\n
  # mentioned in https://geminiprotocol.net/news/2024_07_30.gmi
  # Maybe TODO: streaming, multipart replies etc
  status, header, body_arr = response
  if status === 0
    # blocked
    env[:socket]&.close
    return
  elsif status.nil?
    status = 40
    header = Status::CODE[40]
  end
  body = if body_arr.nil? || !body_arr.is_a?(Array) || body_arr.first.nil?
    ""
  else
    body_arr.first
  end
  env[:socket].puts "#{status} #{header}\r\n#{body}"
rescue
  env[:socket].puts "40 #{Status::CODE[40]}\r\n"
end

#request(socket:) ⇒ Object

creates env hash, request:, config:, server:, valid:



104
105
106
107
108
109
110
111
# File 'lib/above/server.rb', line 104

def request(socket:)
  config = @config["middleware"]
  str = socket.gets.chomp
  request = URI(str)
  {socket:, request:, config:, server: @server_config, valid: true}
rescue URI::InvalidURIError
  {socket:, request: str[0...1023], config:, server: @server_config, valid: false}
end

#serve(tls:) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/above/server.rb', line 113

def serve(tls:)
  barrier = Async::Barrier.new
  Sync do
    on_quit(barrier:)
    limit = Async::Semaphore.new(@server_config["max_fibers"], parent: barrier)
    loop do
      listener(tls:, limit:)
    end
  ensure
    barrier.stop
  end
end

#startObject



126
127
128
# File 'lib/above/server.rb', line 126

def start
  serve(tls: tls_server)
end

#tls_serverObject



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/above/server.rb', line 130

def tls_server
  tcp_server = TCPServer.new(@server_config["ip"], @server_config["port"])
  ssl_ctx = OpenSSL::SSL::SSLContext.new.tap do |ctx|
    ctx.key = OpenSSL::PKey::EC.new(
      File.read(
        File.expand_path(
          @server_config["tls_key"]
        )
      )
    )
    ctx.cert = OpenSSL::X509::Certificate.new(
      File.read(
        File.expand_path(
          @server_config["tls_cert"]
        )
      )
    )
  end
  OpenSSL::SSL::SSLServer.new(tcp_server, ssl_ctx)
rescue => error
  puts "Certificate/Key loading errors - #{error.message}"
  exit
end