diamant 0.0.8 → 0.0.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78504900d38add8a76be57a468a7e3b86284ef964de1e75a6bc40f614649f78b
4
- data.tar.gz: b0212f1c5b7180d0b7de192ab13c5abe37863d6fd5dbde61d5dea86dfcba3572
3
+ metadata.gz: 3aae272eff26676b737dc65ff08ea76b0b4ea1822ec4785dfa47b97f0647dbe8
4
+ data.tar.gz: dc803566505cbed2653c44ac2bc7d4d3e0767ceb4604f91f32c1523917045192
5
5
  SHA512:
6
- metadata.gz: da679165a67588a03666ad9e12149927bdbaf6cf663165693cc42c26a70ec39f7f0e19a9b61214d27ba6b2c9ec13f64b55f8059a2d49d4a539940b82c9b39d29
7
- data.tar.gz: 814580c5b14c5344b1e2432e7eb078e83c1421b939f11437f6a921f94dec7c066bd8329348d259749dacf7ac033e086d9a1d784773a389e4294b9e6794ef5118
6
+ metadata.gz: dc0d90505b8e7ef55f679c290aca6288dddd7c5b3208fbcab86dfa9501be7316ae8f425a3e5e4e5a3525d03c8765c397729231a590836bbe168783f162c3a852
7
+ data.tar.gz: b181654c75861b8a20308513b451999b63ce08b47c527ce2258de451daa16835aba9676c0f0ede70cb609eeb7b1a71f0a8f933b3c031ab39d505e4308396d532
data/bin/diamant CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'diamant'
5
- require 'diamant/cert_generator'
6
4
  require 'optparse'
5
+ require_relative '../lib/diamant'
6
+ require_relative '../lib/diamant/cert_generator'
7
7
 
8
8
  options = { hostname: '127.0.0.1', port: 1965 }
9
9
  OptionParser.new do |parser| # rubocop:disable Metrics/BlockLength
@@ -23,15 +23,15 @@ module Diamant
23
23
  end
24
24
 
25
25
  def write
26
- IO.write('key.rsa', @key.to_pem)
26
+ File.write('key.rsa', @key.to_pem)
27
27
  File.chmod(0o400, 'key.rsa')
28
- IO.write('cert.pem', @cert.to_pem)
28
+ File.write('cert.pem', @cert.to_pem)
29
29
  File.chmod(0o644, 'cert.pem')
30
30
  end
31
31
 
32
32
  private
33
33
 
34
- def init_cert
34
+ def init_cert # rubocop:disable Metrics/AbcSize
35
35
  @cert = OpenSSL::X509::Certificate.new
36
36
  @cert.version = 3
37
37
  @cert.serial = 0x0
@@ -40,7 +40,7 @@ module Diamant
40
40
  @cert.public_key = @key.public_key
41
41
  @cert.not_before = Time.now
42
42
  # 1 years validity
43
- @cert.not_after = @cert.not_before + 1 * 365 * 24 * 60 * 60
43
+ @cert.not_after = @cert.not_before + (1 * 365 * 24 * 60 * 60)
44
44
  @cert
45
45
  end
46
46
 
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diamant
4
+ class MimeError < StandardError; end
5
+
6
+ # MIME aware wrapper for file
7
+ class MimeFile
8
+ attr_reader :body, :category, :content_type, :extension
9
+
10
+ MIMETYPES = {
11
+ '.gemini' => 'text/gemini',
12
+ '.gmi' => 'text/gemini',
13
+ '.txt' => 'text/plain',
14
+ '.md' => 'text/markdown',
15
+ '.org' => 'text/org',
16
+ '.rss' => 'application/rss+xml',
17
+ '.atom' => 'application/atom+xml',
18
+ '.xml' => 'application/xml',
19
+ '.svg' => 'image/svg+xml',
20
+ '.bmp' => 'image/bmp',
21
+ '.png' => 'image/png',
22
+ '.jpg' => 'image/jpeg',
23
+ '.jpeg' => 'image/jpeg',
24
+ '.gif' => 'image/gif',
25
+ '.webp' => 'image/webp',
26
+ '.mp3' => 'audio/mpeg',
27
+ '.ogg' => 'audio/ogg'
28
+ }.freeze
29
+
30
+ def initialize(path)
31
+ @path = path
32
+ @body = File.read path
33
+ @extension, @content_type = extract_info
34
+ @category = classify
35
+ validate!
36
+ prepare_gem_file if @content_type == 'text/gemini'
37
+ end
38
+
39
+ # Disable metrics on purpose for big switch
40
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
41
+ def validate
42
+ return true if @category == :text
43
+
44
+ case @content_type
45
+ when 'image/jpeg'
46
+ @body[0..1] == "\xFF\xD8"
47
+ when 'image/png'
48
+ @body[0..7] == "\x89PNG\r\n\u001A\n"
49
+ when 'image/gif'
50
+ @body[0..2] == 'GIF'
51
+ when 'image/webp'
52
+ @body[0..3] == 'RIFF' && @body[8..11] == 'WEBP'
53
+ when 'image/bmp'
54
+ @body[0..2] == 'BM'
55
+ when 'image/svg+xml', 'application/xml',
56
+ 'application/rss+xml', 'application/atom+xml'
57
+ @body[0..5] == '<?xml '
58
+ when 'audio/mpeg'
59
+ @body[0..3] == 'ID3'
60
+ when 'audio/ogg'
61
+ @body[0..4] == 'OggS'
62
+ else
63
+ false
64
+ end
65
+ end
66
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
67
+
68
+ def validate!
69
+ return if validate
70
+
71
+ raise MimeError, "#{@path} does not looks like a #{@content_type} file!"
72
+ end
73
+
74
+ private
75
+
76
+ def extract_info
77
+ extension = File.extname @path
78
+ mime = MIMETYPES[extension]
79
+ raise MimeError, "#{@path} format is not supported!" if mime == ''
80
+
81
+ # Any other supported extension
82
+ [extension, mime]
83
+ end
84
+
85
+ def classify
86
+ return unless @content_type
87
+
88
+ @content_type.split('/', 2).first.to_sym
89
+ end
90
+
91
+ def prepare_gem_file
92
+ # Ensure each lines finishes with \r\n
93
+ @body = @body.each_line(chomp: true).to_a.join "\r\n"
94
+ end
95
+ end
96
+ end
@@ -3,9 +3,13 @@
3
3
  require 'net/gemini/request'
4
4
  require 'uri/gemini'
5
5
 
6
+ require_relative 'mimefile'
7
+
6
8
  module Diamant
7
9
  # Methods to generate requests responses
8
10
  module Response
11
+ class NotFound < Net::Gemini::BadResponse; end
12
+
9
13
  private
10
14
 
11
15
  def reject_request?(sock, current_load)
@@ -13,6 +17,7 @@ module Diamant
13
17
  return false if current_load < 11
14
18
  # Seppuku
15
19
  raise 'Server is under heavy load' if current_load > 1965
20
+
16
21
  if current_load > 42
17
22
  @logger.warn '41 - Too much threads...'
18
23
  sock.puts "41 See you soon...\r\n"
@@ -24,33 +29,39 @@ module Diamant
24
29
  true
25
30
  end
26
31
 
27
- def read_file(client)
32
+ def build_response(client)
28
33
  r = Net::Gemini::Request.read_new(client)
29
- [r.uri, route(r.path)]
34
+ file_path = route r.path
35
+ [r.uri, read_mime_file(file_path)]
36
+ rescue NotFound => e
37
+ @logger.warn "51 - #{e}"
38
+ [r.uri, "51 Not found!\r\n"]
30
39
  rescue Net::Gemini::BadRequest, URI::InvalidURIError => e
31
40
  @logger.error "59 - #{e}"
32
- [nil, ["59\r\n"]]
41
+ [nil, "59\r\n"]
33
42
  end
34
43
 
35
44
  def route(path)
36
45
  # In any case, remove the / prefix
37
- route = File.expand_path path.delete_prefix('/'), Dir.pwd
46
+ route = File.expand_path path.delete_prefix('/')
38
47
  # We better should use some sort of chroot...
39
48
  unless route.start_with?(Dir.pwd)
40
- @logger.warn "Bad attempt to get something out of public_dir: #{route}"
41
- return ['51 Not found!']
49
+ raise NotFound, "Attempt to get something out of public_dir: #{route}"
42
50
  end
51
+
43
52
  route << '/index.gmi' if File.directory?(route)
44
- return ['51 Not found!'] unless File.exist?(route)
45
- build_response route
53
+ return route if File.exist?(route)
54
+
55
+ raise NotFound, "File #{route} does not exist"
46
56
  end
47
57
 
48
- def build_response(route)
49
- info = Diamant::MimeType.new(route)
50
- answer = IO.readlines route, chomp: true
51
- answer.prepend "20 #{info.content_type}"
52
- rescue Diamant::MimeError
53
- ['50 Not a supported file!']
58
+ def read_mime_file(route)
59
+ info = Diamant::MimeFile.new route
60
+ header = "20 #{info.content_type}"
61
+ [header, info.body].join("\r\n")
62
+ rescue Diamant::MimeError => e
63
+ @logger.error "50 - #{e}"
64
+ "50 Not a supported file!\r\n"
54
65
  end
55
66
  end
56
67
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diamant
4
- VERSION = '0.0.8'
4
+ VERSION = '0.0.10'
5
5
  end
data/lib/diamant.rb CHANGED
@@ -6,9 +6,7 @@ require 'English'
6
6
  require 'openssl'
7
7
  require 'fileutils'
8
8
 
9
- require 'diamant/version'
10
- require 'diamant/mimetype'
11
- require 'diamant/response'
9
+ require_relative 'diamant/response'
12
10
 
13
11
  module Diamant
14
12
  # Runs the server request/answer loop.
@@ -34,13 +32,7 @@ module Diamant
34
32
 
35
33
  def main_loop(ssl_serv)
36
34
  loop do
37
- Thread.new(ssl_serv.accept) do |client|
38
- handle_client(client)
39
- rescue Errno::ECONNRESET, Errno::ENOTCONN => e
40
- @logger.error(e.message)
41
- ensure
42
- client.close
43
- end
35
+ main_loop_tick ssl_serv.accept
44
36
  rescue OpenSSL::SSL::SSLError => e
45
37
  # Do not even try to answer anything as the socket cannot be
46
38
  # built. This will abruptly interrupt the connection from a client
@@ -56,17 +48,26 @@ module Diamant
56
48
  end
57
49
  end
58
50
 
51
+ # Might raises some errors
52
+ def main_loop_tick(connection)
53
+ Thread.new(connection) do |client|
54
+ handle_client(client)
55
+ rescue Errno::ECONNRESET, Errno::ENOTCONN, Errno::ETIMEDOUT => e
56
+ @logger.error(e.message)
57
+ ensure
58
+ client.close
59
+ end
60
+ end
61
+
59
62
  def handle_client(client)
60
- current_load = Thread.list.length - 1
63
+ current_load = Thread.list.length - 1 # Exclude main thread
61
64
  return if reject_request?(client, current_load)
62
65
 
63
- uri, answer = read_file(client)
66
+ uri, answer = build_response(client)
64
67
  log_line = [current_load, client.peeraddr[3], answer[0]]
65
68
  log_line << uri if uri
66
69
  @logger.info log_line.join(' - ')
67
- answer.each do |line|
68
- client.puts "#{line}\r\n"
69
- end
70
+ client.puts answer
70
71
  end
71
72
 
72
73
  def ssl_context
metadata CHANGED
@@ -1,113 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diamant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Étienne Deparis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-20 00:00:00.000000000 Z
11
+ date: 2024-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-net-text
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '='
18
- - !ruby/object:Gem::Version
19
- version: 0.0.9
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.9
27
- - !ruby/object:Gem::Dependency
28
- name: rspec
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '3.12'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '3.12'
41
- - !ruby/object:Gem::Dependency
42
- name: rubocop
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '1.57'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '1.57'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop-performance
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '1.19'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.19'
69
- - !ruby/object:Gem::Dependency
70
- name: rubocop-rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '2.24'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '2.24'
83
- - !ruby/object:Gem::Dependency
84
- name: simplecov
85
15
  requirement: !ruby/object:Gem::Requirement
86
16
  requirements:
87
17
  - - "~>"
88
18
  - !ruby/object:Gem::Version
89
- version: '0.22'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '0.22'
97
- - !ruby/object:Gem::Dependency
98
- name: yard
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: '0.9'
104
- type: :development
19
+ version: '0.1'
20
+ type: :runtime
105
21
  prerelease: false
106
22
  version_requirements: !ruby/object:Gem::Requirement
107
23
  requirements:
108
24
  - - "~>"
109
25
  - !ruby/object:Gem::Version
110
- version: '0.9'
26
+ version: '0.1'
111
27
  description: |
112
28
  Diamant is a server for the Gemini network protocol. It can only serve
113
29
  static files. Internally, it uses the OpenSSL library to handle the TLS
@@ -122,13 +38,17 @@ files:
122
38
  - bin/diamant
123
39
  - lib/diamant.rb
124
40
  - lib/diamant/cert_generator.rb
125
- - lib/diamant/mimetype.rb
41
+ - lib/diamant/mimefile.rb
126
42
  - lib/diamant/response.rb
127
43
  - lib/diamant/version.rb
128
44
  homepage: https://git.umaneti.net/diamant/about/
129
45
  licenses:
130
46
  - WTFPL
131
- metadata: {}
47
+ metadata:
48
+ rubygems_mfa_required: 'true'
49
+ source_code_uri: https://git.umaneti.net/fronde
50
+ homepage_uri: https://etienne.depar.is/fronde/
51
+ funding_uri: https://liberapay.com/milouse
132
52
  post_install_message:
133
53
  rdoc_options: []
134
54
  require_paths:
@@ -144,7 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
64
  - !ruby/object:Gem::Version
145
65
  version: '0'
146
66
  requirements: []
147
- rubygems_version: 3.4.20
67
+ rubygems_version: 3.5.15
148
68
  signing_key:
149
69
  specification_version: 4
150
70
  summary: A simple Gemini server for static files.
@@ -1,41 +0,0 @@
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
- '.gemini' => 'text/gemini',
12
- '.gmi' => 'text/gemini',
13
- '.txt' => 'text/plain',
14
- '.md' => 'text/markdown',
15
- '.org' => 'text/org',
16
- '.xml' => 'application/xml',
17
- '.png' => 'image/png',
18
- '.jpg' => 'image/jpeg',
19
- '.jpeg' => 'image/jpeg',
20
- '.gif' => 'image/gif'
21
- }.freeze
22
-
23
- def initialize(path)
24
- @path = path
25
- extract_info
26
- end
27
-
28
- def supported?
29
- @extension != '' && MIMETYPES.has_key?(@extension)
30
- end
31
-
32
- private
33
-
34
- def extract_info
35
- @extension = File.extname @path
36
- raise MimeError, "#{@path} format is not supported!" unless supported?
37
- # Any other supported extension
38
- @content_type = MIMETYPES[@extension]
39
- end
40
- end
41
- end