bgeminiserver 1.0.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 +7 -0
- data/lib/geminiserver.rb +304 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a10d4befb1cadf285c0a954a7fa848cce404d089eb9270cf8cf3f5d2dad652be
|
4
|
+
data.tar.gz: e568c030f1dfa5fc0b016a9c1d7e879e38a48542e74a3c51357a7ae9b3e281f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f5fe797b57334add1dfb4be97b7f0e5c6d4b15c25f7457f75880cdb969283b9c5e78466ae413795a61d5ca7c6c2deb147631cc1a3685b5704269ca167fc25beb
|
7
|
+
data.tar.gz: 46bc8cba51ede5f3fa4d85238ffce0fc4cb3ba2d55f41be3ebf65c1f22e030a38e1d326a6c9c3c55bd001a2f267cace3dab2abe4ffa28d87323ce893ef336641
|
data/lib/geminiserver.rb
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
|
2
|
+
require "socket"
|
3
|
+
require "openssl"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module GeminiPage
|
7
|
+
|
8
|
+
# Indexes a path of the file sysem (with folders) and creates corresponding
|
9
|
+
# handlers. There is no caching.
|
10
|
+
# All files which do not have a mimetype with text/ are read binary.
|
11
|
+
#
|
12
|
+
# @param serv [GeminiServer] The server object on which the handlers should be
|
13
|
+
# registered.
|
14
|
+
# @param dir_path [String] Path to be indexed.
|
15
|
+
# @param prefix [String] Prefix on the server.
|
16
|
+
# @param extensions [Hash] List of file extensions and corresponding mimetypes.
|
17
|
+
# @param default [String] Default mimetype
|
18
|
+
# @param file_prefix [String] Internal use. Path from the file system which
|
19
|
+
# should not be used as a prefix on the server.
|
20
|
+
# @example Indexes the downloads folder of the user "user" and makes the files
|
21
|
+
# available under "/computer/Downloads/".
|
22
|
+
# GeminiPage.index_dir serv, "/home/user/Downloads/", "/computer/Downloads/"
|
23
|
+
def self.index_dir serv, dir_path, prefix = "/", extensions = {"gmi" => "text/gemini"}, default = "application/octet-stream", file_prefix = nil
|
24
|
+
file_prefix = dir_path if ! file_prefix
|
25
|
+
|
26
|
+
Dir["#{dir_path}/*"].each { |entry|
|
27
|
+
puts entry
|
28
|
+
if File.file?(entry) && File.readable?(entry)
|
29
|
+
serv_path = entry.delete_prefix file_prefix
|
30
|
+
serv_path.delete_prefix! "/"
|
31
|
+
serv.register_handler("#{prefix}#{serv_path}", ->(conn, cert, input) {
|
32
|
+
extension = serv_path.split(".")[-1]
|
33
|
+
mimetype = extensions[extension]
|
34
|
+
mimetype = default if ! mimetype
|
35
|
+
conn.print "20 #{mimetype}\r\n"
|
36
|
+
mode = "r"
|
37
|
+
if ! mimetype.start_with? "text/"
|
38
|
+
mode += "b"
|
39
|
+
end
|
40
|
+
fil = File.open(entry, mode)
|
41
|
+
IO::copy_stream(fil, conn)
|
42
|
+
})
|
43
|
+
else File.directory?(entry)
|
44
|
+
index_dir serv, entry, prefix, extensions, default, file_prefix
|
45
|
+
end
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates a static page without dynamic (therefore interactive) content.
|
50
|
+
#
|
51
|
+
# @param code [String] The status code which will be returned.
|
52
|
+
# @param meta [String] Meta information. For example, the mimetype for status 20.
|
53
|
+
# @param content [String] The content from which the page is created.
|
54
|
+
# @return [Proc]
|
55
|
+
# @example A simple page in Gemini format, which has "Hello World!" as its headline.
|
56
|
+
# GeminiPage.static_page("20", "text/gemini; lang=en", "# Hello World!")
|
57
|
+
def self.static_page code, meta, content = nil
|
58
|
+
page = "#{code} #{meta}\r\n"
|
59
|
+
#content.insert(content.index("\n"), "\r")
|
60
|
+
page += content.to_s
|
61
|
+
|
62
|
+
return ->(conn, cert, input) {conn.print page}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Requests an input from the client and calls the function after successful input.
|
66
|
+
#
|
67
|
+
# @param func [Proc] Function which is called.
|
68
|
+
# @param input_prompt [String] Prompt to be sent to the client.
|
69
|
+
# @param secret [String] True if the input contains sensitive data such as a
|
70
|
+
# password, false otherwise.
|
71
|
+
# @return [Proc]
|
72
|
+
# @example A simple page in Gemini format, which has "Hello World!" as its headline.
|
73
|
+
# GeminiPage.require_input(->(conn, cert, input) {
|
74
|
+
# conn.print "20 text/gemini\r\n"
|
75
|
+
# conn.print "# Input test\n"
|
76
|
+
# conn.print "Your input is #{input}"
|
77
|
+
# }, "Some input", true)
|
78
|
+
def self.require_input func, input_prompt, secret = false
|
79
|
+
return ->(conn, cert, input) {
|
80
|
+
if input == ""
|
81
|
+
code = "10"
|
82
|
+
code = "11" if secret
|
83
|
+
conn.print "#{code} #{input_prompt}\r\n"
|
84
|
+
else
|
85
|
+
func.(conn, cert, input)
|
86
|
+
end
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
# Creates a temporary redirect.
|
91
|
+
#
|
92
|
+
# @param new_location [String]
|
93
|
+
# @return [Proc]
|
94
|
+
# @example
|
95
|
+
# GeminiPage.redirect_permanent("/new_location")
|
96
|
+
def self.redirect_temporary new_location
|
97
|
+
return ->(conn, cert, input) {
|
98
|
+
conn.print "30 #{new_location}\r\n"
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Creates a permanent redirect.
|
103
|
+
#
|
104
|
+
# @param new_location [String]
|
105
|
+
# @return [Proc]
|
106
|
+
# @example
|
107
|
+
# GeminiPage.redirect_permanent("/new_location")
|
108
|
+
def self.redirect_permanent new_location
|
109
|
+
return ->(conn, cert, input) {
|
110
|
+
conn.print "31 #{new_location}\r\n"
|
111
|
+
}
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# @example A test server
|
116
|
+
# cert = OpenSSL::X509::Certificate.new File.read "cert.crt"
|
117
|
+
# key = OpenSSL::PKey::RSA.new File.read "priv.pem"
|
118
|
+
# serv = GeminiServer.new cert, key
|
119
|
+
#
|
120
|
+
# users = {}
|
121
|
+
#
|
122
|
+
# serv.register_handler "/", GeminiPage.static_page("20", "text/gemini; lang=en", <<CONTENT)
|
123
|
+
# # Hello to my Ruby Gemini server
|
124
|
+
#
|
125
|
+
# This is the startpage.
|
126
|
+
# You can take a look at /cert to test the certificate function of the server
|
127
|
+
# or at /input and /inputpw to test the input function of this server :-)
|
128
|
+
# => /cert
|
129
|
+
# => /input
|
130
|
+
# => /inputpw
|
131
|
+
# => /redirect
|
132
|
+
# => /test2
|
133
|
+
#
|
134
|
+
# Hope it works!
|
135
|
+
# CONTENT
|
136
|
+
# # .register_handler "", GeminiPage.redirect_permanent("/")
|
137
|
+
# # or
|
138
|
+
# # serv.copy_handler("/", "")
|
139
|
+
#
|
140
|
+
# serv.register_handler "/cert", ->(conn, cert, input) {
|
141
|
+
# if conn.peer_cert == nil
|
142
|
+
# conn.print "60 Require certificate\r\n"
|
143
|
+
# return
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# conn.print "20 text/gemini\r\n"
|
147
|
+
# conn.puts "# Certificate test\n"
|
148
|
+
# conn.puts "Certificate subject and issuer are equal.\n" if cert.subject == cert.issuer
|
149
|
+
# conn.puts "Serialnumber: #{cert.serial.to_s}\n"
|
150
|
+
# conn.puts "## Subject\n"
|
151
|
+
# cert.subject.to_a.each { |entry|
|
152
|
+
# conn.puts "* #{entry[0]} : #{entry[1]}\n"
|
153
|
+
# }
|
154
|
+
# conn.puts "## Issuer\n"
|
155
|
+
# cert.issuer.to_a.each { |entry|
|
156
|
+
# conn.puts "* #{entry[0]} : #{entry[1]}\n"
|
157
|
+
# }
|
158
|
+
#
|
159
|
+
# if ! users[cert.subject.to_s]
|
160
|
+
# users[cert.subject.to_s] = cert.public_key
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# if cert.verify users[cert.subject.to_s]
|
164
|
+
# conn.puts "You are authenticated as #{cert.subject.to_s}"
|
165
|
+
# else
|
166
|
+
# conn.puts "You are not authenticated as #{cert.subject.to_s}"
|
167
|
+
# end
|
168
|
+
# }
|
169
|
+
#
|
170
|
+
# serv.register_handler "/input", GeminiPage.require_input(->(conn, cert, input) {
|
171
|
+
# conn.print "20 text/gemini\r\n"
|
172
|
+
# conn.print "# Input test\n"
|
173
|
+
# conn.print "Your input is #{input}"
|
174
|
+
# }, "Some input", true)
|
175
|
+
#
|
176
|
+
# serv.register_handler "/inputpw", GeminiPage.require_input(->(conn, cert, input) {
|
177
|
+
# conn.print "20 text/gemini\r\n"
|
178
|
+
# conn.print "# Input test\n"
|
179
|
+
# conn.print "Your input is #{input}"
|
180
|
+
# }, "Some secret input", true)
|
181
|
+
#
|
182
|
+
# serv.register_handler "/test2", ->(conn, cert, input) {
|
183
|
+
# conn.print "20 \r\n"
|
184
|
+
# conn.print "Hello World!"
|
185
|
+
# }
|
186
|
+
#
|
187
|
+
# serv.register_handler "/redirect", GeminiPage.redirect_permanent("/")
|
188
|
+
#
|
189
|
+
# serv.start
|
190
|
+
#
|
191
|
+
# puts "Listen..."
|
192
|
+
# serv.listen true
|
193
|
+
class GeminiServer
|
194
|
+
|
195
|
+
attr_accessor :not_found_page
|
196
|
+
|
197
|
+
# Creates a Gemini Server
|
198
|
+
#
|
199
|
+
# @param cert [OpenSSL::X509::Certificate]
|
200
|
+
# @param key [OpenSSL::PKey::PKey]
|
201
|
+
# @example
|
202
|
+
# cert = OpenSSL::X509::Certificate.new File.read "cert.crt"
|
203
|
+
# key = OpenSSL::PKey::RSA.new File.read "priv.pem"
|
204
|
+
# serv = GeminiServer.new cert, key
|
205
|
+
def initialize cert, key
|
206
|
+
@context = OpenSSL::SSL::SSLContext.new
|
207
|
+
# Require min tls version (spec 4.1)
|
208
|
+
@context.min_version = :TLS1_2
|
209
|
+
@context.add_certificate cert, key
|
210
|
+
# Enable client certificates (spec 4.3)
|
211
|
+
@context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
212
|
+
# Ignore invalid (e. g. self-signed) certificates
|
213
|
+
@context.verify_callback = ->(passed, cert) { return true }
|
214
|
+
@handlers = {}
|
215
|
+
@not_found_page = GeminiPage.static_page("51", "Not found")
|
216
|
+
end
|
217
|
+
|
218
|
+
# Starts the server. However, the server does not yet respond to requests.
|
219
|
+
#
|
220
|
+
# @param host [String]
|
221
|
+
# @param port [Integer]
|
222
|
+
# @example
|
223
|
+
# serv.start
|
224
|
+
def start host = "localhost", port = 1965
|
225
|
+
serv = TCPServer.new host, port
|
226
|
+
@secure = OpenSSL::SSL::SSLServer.new(serv, @context)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Starts the server. However, the server does not yet respond to requests.
|
230
|
+
#
|
231
|
+
# Registers a handler for a specific path. The handler is a function (Proc),
|
232
|
+
# which gets three parameters:
|
233
|
+
# * The current connection to the client. Through this the function can return
|
234
|
+
# content to the client. Can be handled similar to a stream.
|
235
|
+
# * The (if available) certificate sent by the client.
|
236
|
+
# * The input (if any) sent by the client.
|
237
|
+
#
|
238
|
+
# @param path [String]
|
239
|
+
# @param func [Proc]
|
240
|
+
# @example
|
241
|
+
# serv.register_handler "/", ->(conn, cert, input) {
|
242
|
+
# conn.print "20 \r\n"
|
243
|
+
# conn.print "Hello World!"
|
244
|
+
# }
|
245
|
+
def register_handler path, func
|
246
|
+
@handlers[path] = func
|
247
|
+
end
|
248
|
+
|
249
|
+
# Copies a handler for another path. If the original handler is edited, the
|
250
|
+
# new one is not edited.
|
251
|
+
#
|
252
|
+
# @param path [String]
|
253
|
+
# @param copy_path [String]
|
254
|
+
# @example
|
255
|
+
# serv.copy_handler("/", "")
|
256
|
+
def copy_handler path, copy_path
|
257
|
+
@handlers[linked_path] = @handlers[path]
|
258
|
+
end
|
259
|
+
|
260
|
+
# Removes a handler
|
261
|
+
#
|
262
|
+
# @param path [String]
|
263
|
+
def delete_handler path
|
264
|
+
@handlers.delete path
|
265
|
+
end
|
266
|
+
|
267
|
+
# Enables the server so that it responds to requests. This blocks the rest of
|
268
|
+
# the program.
|
269
|
+
#
|
270
|
+
# @param log [TrueClass, FalseClass] If enabled, successful requests from
|
271
|
+
# clients are output.
|
272
|
+
# @example
|
273
|
+
# serv.listen
|
274
|
+
def listen log = false
|
275
|
+
loop do
|
276
|
+
begin
|
277
|
+
Thread.new(@secure.accept) do |conn|
|
278
|
+
begin
|
279
|
+
uri = URI(conn.gets.chomp)
|
280
|
+
|
281
|
+
if uri.scheme != "gemini"
|
282
|
+
conn.print "59 Unknown scheme: #{uri.scheme}\r\n"
|
283
|
+
end
|
284
|
+
|
285
|
+
if @handlers[uri.path]
|
286
|
+
page = @handlers[uri.path].(conn, conn.peer_cert, URI.decode_www_form_component(uri.query.to_s))
|
287
|
+
puts "#{conn.io.peeraddr(false)[-1]} request #{uri.path}"
|
288
|
+
else
|
289
|
+
page = @not_found_page.(conn, conn.peer_cert, URI.decode_www_form_component(uri.query.to_s))
|
290
|
+
end
|
291
|
+
|
292
|
+
conn.flush
|
293
|
+
conn.close
|
294
|
+
rescue
|
295
|
+
$stderr.puts $!
|
296
|
+
end
|
297
|
+
end
|
298
|
+
rescue
|
299
|
+
$stderr.puts $!
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bgeminiserver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marek Kuethe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A small Gemini server, to customize with static and dynamic content.
|
14
|
+
The Gem is documented with example. Before I extended the server, the server was
|
15
|
+
only about 60 lines long. Gemini is a new internet protocol which is heavier than
|
16
|
+
gopher, is lighter than the web, will not replace either, strives for maximum power
|
17
|
+
to weight ratio, takes user privacy very seriously. (gemini://gemini.circumlunar.space/)
|
18
|
+
email: m.k@mk16.de
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- lib/geminiserver.rb
|
24
|
+
homepage: https://github.com/marek22k/geminiserver
|
25
|
+
licenses:
|
26
|
+
- WTFPL
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubygems_version: 3.3.18
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: A small Gemini server, to customize with static and dynamic content.
|
47
|
+
test_files: []
|