gemini_server 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/Gemfile +7 -0
- data/README.md +144 -0
- data/Rakefile +10 -0
- data/bin/gemini_server +41 -0
- data/gemini_server.gemspec +46 -0
- data/lib/gemini_server.rb +242 -0
- data/lib/gemini_server/mime_types.rb +12 -0
- data/lib/gemini_server/responses.rb +76 -0
- data/lib/gemini_server/version.rb +3 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/bin/gemini_server
ADDED
@@ -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
|
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: []
|