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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/geminiserver.rb +304 -0
  3. 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
@@ -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: []