diamant 0.0.1

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