diamant 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a987ba57250c314c052adfa5f913be3312999154e64aa346df39e7c318c41f39
4
+ data.tar.gz: 052f61456c18fdfe8241f3333e81a1c8ab85d7a8fcd8fb4f671179793e5ce1ee
5
+ SHA512:
6
+ metadata.gz: 32ba003a7ce3c3f40f498ad51c7de1a4a292d1f0f13a89855c288ea08555cdfcf5d0bf6ecd39cc08d98097662675af96aa4f74c102f667e7edd19e984b7561e8
7
+ data.tar.gz: 3521229067830e6f91cca8bb0732ae70a2ac9a7cc04072a11b2513b0e1e5b9494300e5f722c4bbdba25c8e651401f068ce30f240acb2b6b161b30ae1a215ea95
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
14
+
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'diamant'
5
+ require 'diamant/cert_generator'
6
+ require 'optparse'
7
+
8
+ options = { hostname: '127.0.0.1', port: 1965 }
9
+ OptionParser.new do |parser| # rubocop:disable Metrics/BlockLength
10
+ parser.banner = "Usage: #{parser.program_name} [options] [ command [ arg ] ]"
11
+
12
+ parser.separator ''
13
+
14
+ parser.separator 'Commands'
15
+
16
+ parser.separator <<~COMMANDSHELP
17
+ serve [ public_path ] Serves static files from the given public_path.
18
+ (defaults to ./public_gmi)
19
+
20
+ generate_tls_cert [ hostname ]
21
+ Generates a new self-signed certificate for the
22
+ given hostname and its related private key.
23
+ (defaults to localhost)
24
+
25
+ If no command is given, or if the command is not recognized, the given
26
+ string is taken as a public_path to serve from. That is:
27
+
28
+ #{parser.program_name} ~/my_gemini_capsule
29
+
30
+ is the same thing than:
31
+
32
+ #{parser.program_name} serve ~/my_gemini_capsule
33
+ COMMANDSHELP
34
+
35
+ parser.separator ''
36
+
37
+ parser.separator 'Options'
38
+
39
+ parser.on('-h', '--help',
40
+ 'Show this help message and quit.') do
41
+ puts parser.help
42
+ exit
43
+ end
44
+
45
+ parser.on('-v', '--version',
46
+ "Show #{parser.program_name} version and quit.") do
47
+ puts Diamant::VERSION
48
+ exit
49
+ end
50
+
51
+ parser.on('-b', '--bind HOST',
52
+ 'Hostname to bind to (127.0.0.1, 0.0.0.0, ::1...).',
53
+ '(defaults to 127.0.0.1)') do |o|
54
+ options[:bind] = o
55
+ end
56
+
57
+ parser.on('-p', '--port PORT',
58
+ 'Define the TCP port to bind to.',
59
+ '(defaults to 1965)') do |o|
60
+ options[:port] = o.to_i
61
+ end
62
+
63
+ parser.on('--cert CERT',
64
+ 'Path to the TLS certificate to use.',
65
+ '(defaults to cert.pem)') do |o|
66
+ options[:cert] = o
67
+ end
68
+
69
+ parser.on('--pkey PKEY',
70
+ 'Path to the TLS private key to use.',
71
+ '(defaults to key.rsa)') do |o|
72
+ options[:pkey] = o
73
+ end
74
+ end.parse!
75
+
76
+ command = ARGV.shift
77
+ case command
78
+ when 'generate_tls_cert'
79
+ Diamant::CertGenerator.new(ARGV[0]).write
80
+ exit
81
+ when 'serve'
82
+ options[:public_path] = ARGV[0]
83
+ else
84
+ options[:public_path] = command
85
+ end
86
+
87
+ options[:public_path] ||= './public_gmi'
88
+
89
+ Diamant::Server.new(options).start
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'socket'
5
+ require 'English'
6
+ require 'openssl'
7
+ require 'fileutils'
8
+
9
+ require 'net/gemini/request'
10
+ require 'uri/gemini'
11
+
12
+ require 'diamant/version'
13
+ require 'diamant/mimetype'
14
+
15
+ module Diamant
16
+ # Runs the server request/answer loop.
17
+ class Server
18
+ def initialize(opts = {})
19
+ @port = opts[:port] || 1965
20
+ @bind = opts[:bind] || '127.0.0.1'
21
+ init_logger
22
+ init_server_paths(opts)
23
+ end
24
+
25
+ def start
26
+ tcp_serv = TCPServer.new @bind, @port
27
+ ssl_serv = OpenSSL::SSL::SSLServer.new tcp_serv, ssl_context
28
+ loop do
29
+ Thread.new(ssl_serv.accept) do |client|
30
+ handle_client(client)
31
+ client.close
32
+ end
33
+ rescue Interrupt
34
+ break
35
+ end
36
+ ensure
37
+ ssl_serv&.shutdown
38
+ end
39
+
40
+ private
41
+
42
+ def handle_client(client)
43
+ begin
44
+ r = Net::GeminiRequest.read_new(client)
45
+ rescue Net::GeminiBadRequest
46
+ client.puts "59\r\n"
47
+ return
48
+ end
49
+ answer = route(r.path)
50
+ @logger.info "#{answer[0]} - #{r.uri}"
51
+ answer.each do |line|
52
+ client.puts "#{line}\r\n"
53
+ end
54
+ end
55
+
56
+ def build_response(route)
57
+ info = Diamant::MimeType.new(route)
58
+ answer = IO.readlines route, chomp: true
59
+ answer.prepend "20 #{info.content_type}"
60
+ rescue Diamant::MimeError
61
+ ['50 Not a supported file!']
62
+ end
63
+
64
+ def route(path)
65
+ # In any case, remove the / prefix
66
+ route = File.expand_path path.delete_prefix('/'), Dir.pwd
67
+ # We better should use some sort of chroot...
68
+ unless route.start_with?(Dir.pwd)
69
+ @logger.warn "Bad attempt to get something out of public_dir: #{route}"
70
+ return ['51 Not found!']
71
+ end
72
+ route << '/index.gmi' if File.directory?(route)
73
+ return ['51 Not found!'] unless File.exist?(route)
74
+ build_response route
75
+ end
76
+
77
+ def ssl_context
78
+ ssl_context = OpenSSL::SSL::SSLContext.new
79
+ ssl_context.min_version = OpenSSL::SSL::TLS1_2_VERSION
80
+ ssl_context.add_certificate @cert, @pkey
81
+ ssl_context
82
+ end
83
+
84
+ def init_logger
85
+ $stdout.sync = true
86
+ @logger = Logger.new($stdout)
87
+ @logger.datetime_format = '%Y-%m-%d %H:%M:%S'
88
+ @logger.formatter = proc do |severity, datetime, _, msg|
89
+ "[#{datetime}] #{severity}: #{msg}\n"
90
+ end
91
+ end
92
+
93
+ def check_option_path_exist(option, default)
94
+ path = File.expand_path(option || default)
95
+ return path if File.exist?(path)
96
+ raise ArgumentError, "#{path} does not exist!"
97
+ end
98
+
99
+ def init_server_paths(opts = {})
100
+ cert_file = check_option_path_exist(opts[:cert], 'cert.pem')
101
+ @cert = OpenSSL::X509::Certificate.new File.read(cert_file)
102
+ key_file = check_option_path_exist(opts[:pkey], 'key.rsa')
103
+ @pkey = OpenSSL::PKey::RSA.new File.read(key_file)
104
+ public_path = check_option_path_exist(
105
+ opts[:public_path], './public_gmi'
106
+ )
107
+ Dir.chdir(public_path)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Diamant
6
+ # Creates a new self-signed certificate and its related RSA private key,
7
+ # suitable to be used as certificate for the Gemini network protocol.
8
+ #
9
+ # This Generator is not intended to advance use as it offers no
10
+ # configuration at all. It use the following options:
11
+ #
12
+ # - 4096 bits RSA key
13
+ # - 1 year validity
14
+ # - self signed certificate
15
+ #
16
+ class CertGenerator
17
+ def initialize(subject = 'localhost')
18
+ @subject = OpenSSL::X509::Name.parse "/CN=#{subject}"
19
+ @key = OpenSSL::PKey::RSA.new 4096
20
+ init_cert
21
+ add_extensions
22
+ @cert.sign @key, OpenSSL::Digest.new('SHA256')
23
+ end
24
+
25
+ def write
26
+ IO.write('key.rsa', @key.to_pem)
27
+ File.chmod(0o400, 'key.rsa')
28
+ IO.write('cert.pem', @cert.to_pem)
29
+ File.chmod(0o644, 'cert.pem')
30
+ end
31
+
32
+ private
33
+
34
+ def init_cert
35
+ @cert = OpenSSL::X509::Certificate.new
36
+ @cert.version = 3
37
+ @cert.serial = 0x0
38
+ @cert.issuer = @subject
39
+ @cert.subject = @subject
40
+ @cert.public_key = @key.public_key
41
+ @cert.not_before = Time.now
42
+ # 1 years validity
43
+ @cert.not_after = @cert.not_before + 1 * 365 * 24 * 60 * 60
44
+ @cert
45
+ end
46
+
47
+ def add_extension_to_cert(ext_factory, name, value, critical: false)
48
+ @cert.add_extension(
49
+ ext_factory.create_extension(name, value, critical)
50
+ )
51
+ end
52
+
53
+ def add_extensions
54
+ ef = OpenSSL::X509::ExtensionFactory.new
55
+ ef.subject_certificate = @cert
56
+ ef.issuer_certificate = @cert
57
+ add_extension_to_cert(
58
+ ef, 'basicConstraints', 'CA:TRUE', critical: true
59
+ )
60
+ add_extension_to_cert(ef, 'subjectKeyIdentifier', 'hash')
61
+ add_extension_to_cert(
62
+ ef, 'authorityKeyIdentifier', 'keyid:always,issuer:always'
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diamant
4
+ class MimeError < StandardError; end
5
+
6
+ # Helper to understand what mimetype has a given file
7
+ class MimeType
8
+ attr_reader :extension, :content_type
9
+
10
+ MIMETYPES = {
11
+ '.gmi' => 'text/gemini',
12
+ '.txt' => 'text/plain',
13
+ '.md' => 'text/markdown',
14
+ '.org' => 'text/org',
15
+ '.png' => 'image/png',
16
+ '.jpg' => 'image/jpeg',
17
+ '.jpeg' => 'image/jpeg',
18
+ '.gif' => 'image/gif'
19
+ }.freeze
20
+
21
+ def initialize(path)
22
+ @path = path
23
+ extract_info
24
+ end
25
+
26
+ def supported?
27
+ @extension == '' || MIMETYPES.has_key?(@extension)
28
+ end
29
+
30
+ private
31
+
32
+ def extract_info
33
+ @extension = File.extname @path
34
+ raise MimeError, "#{@path} format is not supported!" unless supported?
35
+ if @extension == ''
36
+ # Weird... but we'll try the simple way
37
+ @content_type = 'text/plain'
38
+ else
39
+ # Any other supported extension
40
+ @content_type = MIMETYPES[@extension]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diamant
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: diamant
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Étienne Deparis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby-net-text
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-doc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :development
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: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.3'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-performance
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.19'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.19'
139
+ - !ruby/object:Gem::Dependency
140
+ name: yard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.9'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.9'
153
+ description: |
154
+ Diamant is a server for the Gemini network protocol. it
155
+ can only serve static files. Internally, it uses the OpenSSL library to
156
+ handle the TLS sessions, and threads to handle concurrent requests.
157
+ email: etienne@depar.is
158
+ executables:
159
+ - diamant
160
+ extensions: []
161
+ extra_rdoc_files: []
162
+ files:
163
+ - LICENSE
164
+ - bin/diamant
165
+ - lib/diamant.rb
166
+ - lib/diamant/cert_generator.rb
167
+ - lib/diamant/mimetype.rb
168
+ - lib/diamant/version.rb
169
+ homepage: https://git.umaneti.net/diamant/about/
170
+ licenses:
171
+ - WTFPL
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '2.7'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.1.4
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: A simple Gemini server for static files.
192
+ test_files: []