bgeminiserver 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []