diamant 0.0.8 → 0.0.10

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