gemini_server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 29f2c81f5db11cffa8974cf4a1a48654ac00565b66f2ce306086f900246b34f9
4
+ data.tar.gz: 842fee3d9d59845cc851b65c2dc918a4ee850e5ffd6f27e4b1c09131a1181659
5
+ SHA512:
6
+ metadata.gz: 62be452faaec4d6c4b59a15f3847f6776f795a57d7037ec7ff54452ba15ec1ad3410e645e0c595c2358dd53ff0da54410765be27e6448f3519cc5c0ebb857edb
7
+ data.tar.gz: 1cce37852838f92ced3fb4e504ff4356d0d098ef27a47d6638d6cb0123b4d2b8c8a63a0412260c15c71da9c78ab37df8f267328e9c331c3d5643614e95510cbe
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
11
+ *.py
12
+ ssl
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gemini_server.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
@@ -0,0 +1,144 @@
1
+ # GeminiServer
2
+
3
+ A simple server for the Gemini protocol, with an API inspired by Sinatra.
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ require "gemini_server"
9
+
10
+ server = GeminiServer.new
11
+
12
+ server.route("/hithere/:friend") do
13
+ if params["friend"] == "Gary"
14
+ gone "Gary and I aren't on speaking terms, sorry."
15
+ else
16
+ success "Hi there, #{params["friend"]}!"
17
+ end
18
+ end
19
+
20
+ server.route("/byebye") do
21
+ lang "pig-latin"
22
+ success "arewellfay!"
23
+ end
24
+
25
+ server.listen("0.0.0.0", 1965)
26
+ ```
27
+
28
+ ### Initialization options
29
+
30
+ <dl>
31
+ <dt><code>cert</code>*</dt>
32
+ <dd>A SSL certificate. Either a <code>OpenSSL::X509::Certificate</code> object, or a string.</dd>
33
+ <dt><code>cert_path</code>*</dt>
34
+ <dd>Path to a SSL certificate file. Defaults to the value of the env variable <code>GEMINI_CERT_PATH</code>. Ignored if <code>cert</code> option is supplied.</dd>
35
+ <dt><code>key</code>*</dt>
36
+ <dd>A SSL key. Either a <code>OpenSSL::PKey</code> object, or a string.</dd>
37
+ <dt><code>key_path</code>*</dt>
38
+ <dd>Path to a private key file. Defaults to the value of the env variable <code>GEMINI_KEY_PATH</code>. Ignored if <code>key</code> option is supplied.</dd>
39
+ <dt><code>mime_type</code></dt>
40
+ <dd>Sets the default MIME type for successful responses. Defaults to <code>text/gemini</code>, or inferred by the name of the file being served.</dd>
41
+ <dt><code>charset</code></dt>
42
+ <dd>If set, includes the charset in the response's MIME type.</dd>
43
+ <dt><code>lang</code></dt>
44
+ <dd>If set, includes the language in the response's MIME type, if the MIME type is <code>text/gemini</code>. Per the Gemini spec, <em>"Valid values for the "lang" parameter are comma-separated lists of one or more language tags as defined in RFC4646."</em></dd>
45
+ <dt><code>public_folder</code></dt>
46
+ <dd>Path to a location from which the server will serve static files. If not set, the server will not serve any static files.</dd>
47
+ <dt><code>views_folder</code></dt>
48
+ <dd>Path to the location of ERB templates. If not set, defaults to current directory.</dd>
49
+ </dl>
50
+
51
+ *\* The option pairs `cert` and `cert_path`, and likewise `key` and `key_path`, are mutually exclusive, so they are technically optional. But per the Gemini spec, connections must use TLS, so it is a runtime error if neither option, nor either of the fallback env variables, are used.*
52
+
53
+ ### Route handlers
54
+
55
+ To define a route handler, use `GeminiServer#route`:
56
+
57
+ ```ruby
58
+ server = GeminiServer.new
59
+ server.route("/path/to/route/:variable") do
60
+ # route logic
61
+ end
62
+ ```
63
+
64
+ The route method takes a [Mustermann](https://github.com/sinatra/mustermann) matcher string and a block.
65
+
66
+ Within the block, code has access to these methods:
67
+
68
+ <dl>
69
+ <dt><code>params</code></dt>
70
+ <dd>Returns a hash of params parsed from the request path.</dd>
71
+ <dt><code>uri</code></dt>
72
+ <dd>Returns the full URI of the request.</dd>
73
+ <dt><code>mime_type(type)</code></dt>
74
+ <dd>Sets the MIME type of the response.</dd>
75
+ <dt><code>charset(ch)</code></dt>
76
+ <dd>Sets the charset of the response, overriding the server's default charset.</dd>
77
+ <dt><code>lang(l)</code></dt>
78
+ <dd>Sets the lang of the response, overriding the server's default lang.</dd>
79
+ <dt><code>erb(filename, locals: {})</code></dt>
80
+ <dd>Renders an ERB template located at <code>filename</code>, then sets status to success. MIME type is inferred by the template extension. The template will have access to any instance variables defined in the handler block, as well as any local variables passed in via the <code>locals</code> keyword param.</dd>
81
+ <dt><code>respond(code, meta, body=nil)</code></dt>
82
+ <dd>Sets the response code, meta, and optional body. It's probably easier to use <code>erb</code> method, or any of the convenience status methods in the next section.</dd>
83
+ </dl>
84
+
85
+ ### ERB templates
86
+
87
+ Using an ERB template automatically sets the status to `20` (success) because a success is the only type of response that can contain a body. It also tries to infer the MIME type from the template extension (excluding any `.erb`).
88
+
89
+ ERB rendering can define local variables, like in Sinatra:
90
+
91
+ ```ruby
92
+ server.route("/hithere/:friend") do
93
+ erb "hithere.gmi", locals: { friend: params["friend"] }
94
+ end
95
+ ```
96
+
97
+ ```markdown
98
+ <!-- hithere.gmi.erb -->
99
+ # Hi there!
100
+
101
+ Hi there, <%= friend %>.
102
+ ```
103
+
104
+ ERB templates have the `params` hash available as a local var:
105
+
106
+ ```ruby
107
+ server.route("/hithere/:friend") do
108
+ erb "hithere.gmi"
109
+ end
110
+ ```
111
+
112
+ ```markdown
113
+ <!-- hithere.gmi.erb -->
114
+ # Hi there!
115
+
116
+ Hi there, <%= params["friend"] %>.
117
+ ```
118
+
119
+ ### Status methods
120
+
121
+ Each of these methods are available within a route handler block. Forgetting to use a status method defaults to a temporary failure. See [Gemini Specification](https://gemini.circumlunar.space/docs/specification.html) for an explanation of each response status.
122
+
123
+ * `input(prompt)`
124
+ * `sensitive_input(prompt)`
125
+ * `success(body, mime_type=nil)`
126
+ * `redirect_temporary(url)`
127
+ * `redirect_permanent(url)`
128
+ * `temporary_failure(explanation = "Temporary failure")`
129
+ * `server_unavailable(explanation = "Server unavailable")`
130
+ * `cgi_error(explanation = "CGI error")`
131
+ * `proxy_error(explanation = "Proxy error")`
132
+ * `slow_down(delay)`
133
+ * `permanent_failure(explanation = "Permanent failure")`
134
+ * `not_found(explanation = "Not found")`
135
+ * `gone(explanation = "Gone")`
136
+ * `proxy_request_refused(explanation = "Proxy request refused")`
137
+ * `bad_request(explanation = "Bad request")`
138
+ * `client_certificate_required(explanation = "Client certificate )`
139
+ * `certificate_not_authorized(explanation = "Certificate not )`
140
+ * `certificate_not_valid(explanation = "Certificate not valid")`
141
+
142
+ ### Static file serving
143
+
144
+ To serve static files, set the initialization option `public_folder` to the location of your static files. If no route handlers match a request, the server will look for a static file to serve in that location instead. If the `public_folder` option is unset, no static files will be served.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ require "gemini_server"
3
+ require "optparse"
4
+
5
+ options = {
6
+ port: 1965,
7
+ cert_path: ENV["GEMINI_CERT_PATH"],
8
+ key_path: ENV["GEMINI_KEY_PATH"],
9
+ }
10
+
11
+ OptionParser.new do |opts|
12
+ opts.on("-p PORT", "--port PORT", Integer, "Port to listen on") do |port|
13
+ options[:port] = port
14
+ end
15
+
16
+ opts.on("--cert-path PATH", String, "Path to cert file") do |path|
17
+ options[:cert_path] = path
18
+ end
19
+
20
+ opts.on("--key-path PATH", String, "Path to key file") do |path|
21
+ options[:key_path] = path
22
+ end
23
+
24
+ opts.on("--charset CHARSET", String, "Charset of text/* files") do |charset|
25
+ options[:charset] = charset
26
+ end
27
+
28
+ opts.on("--lang LANG", String, "Language of text/* files") do |lang|
29
+ options[:lang] = lang
30
+ end
31
+ end.parse!
32
+
33
+ server = GeminiServer.new(
34
+ public_folder: ".",
35
+ cert_path: options[:cert_path],
36
+ key_path: options[:key_path],
37
+ charset: options[:charset],
38
+ lang: options[:lang],
39
+ )
40
+
41
+ server.listen("127.0.0.1", options[:port])
@@ -0,0 +1,46 @@
1
+ require_relative 'lib/gemini_server/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "gemini_server"
5
+ s.version = GeminiServer::VERSION
6
+ s.authors = ["Jess Bees"]
7
+ s.email = ["hi@toomanybees.com"]
8
+
9
+ s.summary = "Simple server for the Gemini protocol"
10
+ s.description = ""
11
+ s.homepage = "http://github.com/TooManyBees/gemini_server"
12
+ s.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ s.metadata["homepage_uri"] = s.homepage
15
+ s.metadata["source_code_uri"] = s.homepage
16
+ # s.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ s.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ s.require_paths = ["lib"]
24
+ s.executables << 'gemini_server'
25
+ s.date = "2020-11-16"
26
+ s.licenses = ["MIT".freeze]
27
+
28
+ if s.respond_to? :specification_version then
29
+ s.specification_version = 4
30
+ end
31
+
32
+ if s.respond_to? :add_runtime_dependency then
33
+ s.add_runtime_dependency("addressable", ["~> 2.7"])
34
+ s.add_runtime_dependency("async-io", ["~> 1.30"])
35
+ s.add_runtime_dependency("mime-types", ["~> 3.3"])
36
+ s.add_runtime_dependency("mustermann", ["~> 1.1"])
37
+ s.add_development_dependency("localhost", ["~> 1.1"])
38
+ else
39
+ s.add_dependency("addressable", ["~> 2.7"])
40
+ s.add_dependency("async-io", ["~> 1.30"])
41
+ s.add_dependency("mime-types", ["~> 3.3"])
42
+ s.add_dependency("mustermann", ["~> 1.1"])
43
+ s.add_dependency("localhost", ["~> 1.1"])
44
+ end
45
+ end
46
+
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "addressable/uri"
4
+ require "async/io"
5
+ require "async/io/stream"
6
+ require "erb"
7
+ require "mime/types"
8
+ require "mustermann"
9
+ require "openssl"
10
+ require_relative "gemini_server/mime_types"
11
+ require_relative "gemini_server/responses"
12
+
13
+ class GeminiServer
14
+ def initialize options = {}
15
+ @routes = []
16
+ @public_folder = File.expand_path(options[:public_folder]) rescue nil
17
+ @views_folder = File.expand_path(options[:views_folder] || ".")
18
+ @charset = options[:charset]
19
+ @lang = options[:lang]
20
+ @ssl_cert, @ssl_key = self.load_cert_and_key(options)
21
+ end
22
+
23
+ def route r, &blk
24
+ raise "Missing required block for route #{r}" if blk.nil?
25
+ @routes << [Mustermann.new(r), blk]
26
+ end
27
+
28
+ def listen host, port
29
+ Async do
30
+ endpoint = Async::IO::Endpoint.tcp(host, port)
31
+ endpoint = Async::IO::SSLEndpoint.new(endpoint, ssl_context: self.ssl_context(@ssl_cert, @ssl_key))
32
+
33
+ ["INT", "TERM"].each do |signal|
34
+ old_handler = Signal.trap(signal) do
35
+ @server.stop if @server
36
+ old_handler.call if old_handler.respond_to?(:call)
37
+ end
38
+ end
39
+
40
+ @server = Async do |task|
41
+ endpoint.accept do |client|
42
+ start_time = clock_time
43
+ remote_ip = client.connect.io.remote_address.ip_address
44
+ io = Async::IO::Stream.new(client.connect)
45
+ status, size, uri, captured_error = handle_request(io, client)
46
+ puts log(ip: remote_ip, uri: uri, start_time: start_time, status: status, body_size: size)
47
+ raise captured_error if captured_error.is_a?(Exception)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ MAX_URL_SIZE = 1024
56
+
57
+ def read_request_url io
58
+ buf = String.new
59
+ offset = 0
60
+ while buf.length <= MAX_URL_SIZE + 2 do
61
+ buf << io.read_partial
62
+ line_end = buf.index("\r\n", offset)
63
+ offset = buf.length
64
+ return buf[0...line_end] if line_end && line_end <= MAX_URL_SIZE
65
+ end
66
+ end
67
+
68
+ def handle_request io, client
69
+ data = read_request_url(io)
70
+ if data.nil?
71
+ ctx = ResponseContext.new({}, nil)
72
+ ctx.bad_request "URI too long"
73
+ return send_response(client, ctx.response)
74
+ end
75
+ status, size, captured_error = nil
76
+ uri = begin
77
+ Addressable::URI.parse(data)
78
+ rescue Addressable::URI::InvalidURIError
79
+ ctx = ResponseContext.new({}, nil)
80
+ ctx.bad_request "Invalid URI"
81
+ return send_response(client, ctx.response)
82
+ end
83
+ uri.scheme = "gemini" if uri.scheme.nil?
84
+ params, handler = self.find_route(uri.path)
85
+ if params
86
+ ctx = ResponseContext.new(params, uri, views_folder: @views_folder, charset: @charset, lang: @lang)
87
+ begin
88
+ ctx.instance_exec(&handler)
89
+ rescue StandardError => e
90
+ ctx.temporary_failure
91
+ captured_error = e
92
+ ensure
93
+ status, size = send_response(client, ctx.response)
94
+ end
95
+ elsif static_response = serve_static(uri.path)
96
+ status, size = send_response(client, static_response)
97
+ else
98
+ ctx = ResponseContext.new(params, uri)
99
+ ctx.not_found
100
+ status, size = send_response(client, ctx.response)
101
+ end
102
+ [status, size, uri, captured_error]
103
+ end
104
+
105
+ def serve_static path
106
+ return if @public_folder.nil?
107
+ path = File.expand_path "#{@public_folder}#{path}"
108
+ return unless path.start_with?(@public_folder)
109
+ File.open(path) do |f|
110
+ mime_type = MIME::Types.type_for(File.basename(path)).first || "text/plain"
111
+ { code: 20, meta: mime_type, body: f.read }
112
+ end
113
+ rescue Errno::ENOENT, Errno::ENAMETOOLONG, Errno::EISDIR # TODO: index.gmi?
114
+ rescue SystemCallError
115
+ { code: 40, meta: "Temporary failure" }
116
+ end
117
+
118
+ def find_route path
119
+ @routes.each do |(route, handler)|
120
+ params = route.params(path)
121
+ return [params, handler] if params
122
+ end
123
+ nil
124
+ end
125
+
126
+ def send_response client, response
127
+ body_size = nil
128
+ if (20...30).include?(response[:code])
129
+ mime_type = MIME::Types[response[:mime_type] || response[:meta]].first || GEMINI_MIME_TYPE
130
+ client.write "#{response[:code]} #{mime_type}"
131
+ if mime_type.media_type == "text"
132
+ client.write "; charset=#{response[:charset]}" if response[:charset]
133
+ client.write "; lang=#{response[:lang]}" if response[:lang] && mime_type.sub_type == "gemini"
134
+ end
135
+ client.write "\r\n"
136
+ body_size = client.write response[:body]
137
+ else
138
+ client.write "#{response[:code]} #{response[:meta]}\r\n"
139
+ end
140
+ client.close
141
+ [response[:code], body_size]
142
+ end
143
+
144
+ LOG_FORMAT = "%s - %s [%s] \"%s\" %s %s %0.4f"
145
+
146
+ def log ip:, uri:, start_time:, username:nil, status:, body_size:nil
147
+ # Imitates Apache common log format to the extent that it applies to Gemini
148
+ # http://httpd.apache.org/docs/1.3/logs.html#common
149
+ path = uri ? uri.omit(:scheme, :host).to_s : '<uri too long>'
150
+ path = path.length > 0 ? path : "/"
151
+ LOG_FORMAT % [ip, username || '-', Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"), path, status.to_s, body_size.to_s || '-', clock_time - start_time]
152
+ end
153
+
154
+ def load_cert_and_key options
155
+ found_cert = options[:cert] || if options[:cert_path]
156
+ File.open(options[:cert_path]) rescue nil
157
+ elsif ENV["GEMINI_CERT_PATH"]
158
+ File.open(ENV["GEMINI_CERT_PATH"]) rescue nil
159
+ end
160
+
161
+ found_key = options[:key] || if options[:key_path]
162
+ File.open(options[:key_path]) rescue nil
163
+ elsif ENV["GEMINI_KEY_PATH"]
164
+ File.open(ENV["GEMINI_KEY_PATH"]) rescue nil
165
+ end
166
+
167
+ raise "SSL certificate not found" unless found_cert
168
+ raise "SSL key not found" unless found_key
169
+
170
+ [
171
+ found_cert.is_a?(OpenSSL::X509::Certificate) ? found_cert : OpenSSL::X509::Certificate.new(found_cert),
172
+ found_key.is_a?(OpenSSL::PKey::PKey) ? found_key : OpenSSL::PKey.read(found_key),
173
+ ]
174
+ end
175
+
176
+ def ssl_context cert, key
177
+ OpenSSL::SSL::SSLContext.new.tap do |context|
178
+ context.add_certificate(cert, key)
179
+ context.session_id_context = "gemini_server"
180
+ context.min_version = OpenSSL::SSL::TLS1_2_VERSION
181
+ context.max_version = OpenSSL::SSL::TLS1_3_VERSION
182
+ context.setup
183
+ end
184
+ end
185
+
186
+ if defined?(Process::CLOCK_MONOTONIC)
187
+ def clock_time
188
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
189
+ end
190
+ else
191
+ def clock_time
192
+ Time.now.to_f
193
+ end
194
+ end
195
+ end
196
+
197
+ class ResponseContext
198
+ include Responses
199
+
200
+ def initialize params, uri, options={}
201
+ @__params = params
202
+ @__uri = uri
203
+ @__mime_type = options[:mime_type]
204
+ @__charset = options[:charset]
205
+ @__lang = options[:lang]
206
+ @__views_folder = options[:views_folder]
207
+ temporary_failure
208
+ end
209
+
210
+ def mime_type t
211
+ type = MIME::Types[t].first
212
+ if type
213
+ @__mime_type = type
214
+ else
215
+ STDERR.puts("WARN: Unknown MIME type #{t.inspect}")
216
+ end
217
+ end
218
+
219
+ def params; @__params; end
220
+ def uri; @__uri; end
221
+ def charset c; @__charset = c; end
222
+ def lang l; @__lang = l; end
223
+
224
+ def erb template, locals: {}
225
+ b = TOPLEVEL_BINDING.dup
226
+ b.local_variable_set(:params, params)
227
+ locals.each { |key, val| b.local_variable_set(key, val) }
228
+ template = File.basename(template.to_s, ".erb")
229
+ mime_type = MIME::Types.type_for(template).first || GEMINI_MIME_TYPE
230
+ t = ERB.new(File.read(File.join(@__views_folder, "#{template}.erb")))
231
+ body = t.result(b)
232
+ success(body, mime_type)
233
+ end
234
+
235
+ def respond code, meta, body=nil
236
+ @__response = { code: code, meta: meta, body: body }
237
+ end
238
+
239
+ def response
240
+ @__response.to_h.merge({ mime_type: @__mime_type, lang: @__lang, charset: @__charset })
241
+ end
242
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mime/types"
4
+
5
+ GEMINI_MIME_TYPE = MIME::Type.new({
6
+ "content-type" => "text/gemini",
7
+ "extensions" => ["gmi", "gemini"],
8
+ "preferred-extension" => "gemini",
9
+ "friendly" => "Gemtext"
10
+ })
11
+
12
+ MIME::Types.add(GEMINI_MIME_TYPE, :silent)
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Responses
4
+ def input prompt
5
+ respond 10, prompt
6
+ end
7
+
8
+ def sensitive_input prompt
9
+ respond 11, prompt
10
+ end
11
+
12
+ def success body, mime_type=nil
13
+ respond 20, mime_type, body
14
+ end
15
+
16
+ def redirect_temporary url
17
+ respond 30, url
18
+ end
19
+
20
+ def redirect_permanent url
21
+ respond 31, url
22
+ end
23
+
24
+ def temporary_failure explanation = "Temporary failure"
25
+ respond 40, explanation
26
+ end
27
+
28
+ def server_unavailable explanation = "Server unavailable"
29
+ respond 41, explanation
30
+ end
31
+
32
+ def cgi_error explanation = "CGI error"
33
+ respond 42, explanation
34
+ end
35
+
36
+ def proxy_error explanation = "Proxy error"
37
+ respond 43, explanation
38
+ end
39
+
40
+ def slow_down delay
41
+ respond 44, delay
42
+ end
43
+
44
+ def permanent_failure explanation = "Permanent failure"
45
+ respond 50, explanation
46
+ end
47
+
48
+ def not_found explanation = "Not found"
49
+ respond 51, explanation
50
+ end
51
+
52
+ def gone explanation = "Gone"
53
+ respond 52, explanation
54
+ end
55
+
56
+ def proxy_request_refused explanation = "Proxy request refused"
57
+ respond 53, explanation
58
+ end
59
+
60
+ def bad_request explanation = "Bad request"
61
+ respond 59, explanation
62
+ end
63
+
64
+ def client_certificate_required explanation = "Client certificate required"
65
+ respond 60, explanation
66
+ end
67
+ alias certificate_required client_certificate_required
68
+
69
+ def certificate_not_authorized explanation = "Certificate not authorized"
70
+ respond 61, explanation
71
+ end
72
+
73
+ def certificate_not_valid explanation = "Certificate not valid"
74
+ respond 62, explanation
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module GeminiServer
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gemini_server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jess Bees
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: addressable
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: async-io
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.30'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.30'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mime-types
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mustermann
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: localhost
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.1'
83
+ description: ''
84
+ email:
85
+ - hi@toomanybees.com
86
+ executables:
87
+ - gemini_server
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - Gemfile
93
+ - README.md
94
+ - Rakefile
95
+ - bin/gemini_server
96
+ - gemini_server.gemspec
97
+ - lib/gemini_server.rb
98
+ - lib/gemini_server/mime_types.rb
99
+ - lib/gemini_server/responses.rb
100
+ - lib/gemini_server/version.rb
101
+ homepage: http://github.com/TooManyBees/gemini_server
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ homepage_uri: http://github.com/TooManyBees/gemini_server
106
+ source_code_uri: http://github.com/TooManyBees/gemini_server
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 2.3.0
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.1.4
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: Simple server for the Gemini protocol
126
+ test_files: []