ruby-net-text 0.1.1 → 0.1.2

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: a5e45e34116b3865c72a1691baa06372be3266aebdae1fc7d823ba4136d4438a
4
- data.tar.gz: 3e8045ac8d2b7c82b4dfffccaeeda707d92aded2f36b7eff794008d0ec8e5b06
3
+ metadata.gz: d42363c44449f94239e0f6ab431c925bf663ed11ee823d1fb797711011401795
4
+ data.tar.gz: ac7bcf1f19392f5a57efab70f71b340680cb51b21d946af00a66dbf20b49ca7e
5
5
  SHA512:
6
- metadata.gz: cbd55419137361ce17b1df2459a734fbd305fad146701fd37b20df22e7fe861daead41494c7e20ce76baf2d37c825667acc87cebe49ef1310001f38b703f1152
7
- data.tar.gz: 6cc15cc19dc559ed63b092f9c1c0c6622685f6a07d90a5634b9f074ca02e762bc4c60be95e8f6d45192718c2797b039e5ea3b13030d52d16b7c585e72b628065
6
+ metadata.gz: 934907f91ea8c4d00415f7a58d238dfadda9dd5bfd017b74fdbaab530eebbc20eb1e0791ead39db5b4637eaa89bdec13fb90e6cf64dd08332ed13b7deddf61df
7
+ data.tar.gz: b0aa7b75364c0ba4d56be615b1d1db77b32f515d5d2a73096c1900d82509a8f7f658f264c81294e833a24cbb421e025ef9ad69c41f8d3becc28b58bd25ac1b5c
data/README.md CHANGED
@@ -1,9 +1,6 @@
1
1
  # Finger, Gemini, Gopher and Nex support for Net::* and URI::*
2
2
 
3
- [![Support using Liberapay](https://img.shields.io/badge/Liberapay-Support_me-yellow?logo=liberapay)](https://liberapay.com/milouse/donate)
4
- [![Support using Flattr](https://img.shields.io/badge/Flattr-Support_me-brightgreen?logo=flattr)](https://flattr.com/@milouse)
5
3
  [![Support using Paypal](https://img.shields.io/badge/Paypal-Support_me-00457C?logo=paypal&labelColor=lightgray)](https://paypal.me/milouse)
6
-
7
4
  [![Gem](https://img.shields.io/gem/v/ruby-net-text)](https://rubygems.org/gems/ruby-net-text)
8
5
  [![Documentation](https://img.shields.io/badge/Documentation-ruby--net--text-CC342D?logo=rubygems)](https://www.rubydoc.info/gems/ruby-net-text/)
9
6
 
@@ -65,3 +62,33 @@ This repository also includes 2 little helpers:
65
62
  and it will output the remote file.
66
63
  - `bin/test_thread.rb`: a toy performance test script to run against a Gemini
67
64
  server
65
+
66
+ ## Contributing
67
+
68
+ ### Code of conduct
69
+
70
+ ![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)
71
+
72
+ `ruby-net-text` is proudly following the
73
+ [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/).
74
+
75
+ [You can read it here](./CODE_OF_CONDUCT.md).
76
+
77
+ ### Code
78
+
79
+ The canonical repository is located at https://git.umaneti.net/ruby-net-text.
80
+ A mirror also exists on [Github](https://github.com/milouse/ruby-net-text).
81
+
82
+ Merge requests and issues are accepted on github, or by mail to the main author.
83
+
84
+ Contributions must not include content generated by large language models. This
85
+ policy covers code, documentation, pull requests, issues, comments, and any
86
+ other contributions to ruby-net-text. The main reason for this, sufficient by
87
+ itself, is ethical issues. AI tools require an unreasonable amount of energy and
88
+ water to build and operate; their models are built with heavily exploited
89
+ workers in unacceptable working conditions. These are harms that we do not want
90
+ to perpetuate, even if only indirectly.¹
91
+
92
+ ¹ This paragraph only is adapted from
93
+ [the Servo project note on AI contribution](https://book.servo.org/contributing/getting-started.html#ai-contributions),
94
+ available under the [Mozilla Public License 2.0](https://github.com/servo/servo/blob/main/LICENSE).
@@ -7,7 +7,7 @@ module Net
7
7
  class Client
8
8
  private
9
9
 
10
- def ssl_check_existing(new_cert, cert_file)
10
+ def ssl_known_cert_valid?(new_cert, cert_file)
11
11
  raw = File.read cert_file
12
12
  saved_one = OpenSSL::X509::Certificate.new raw
13
13
  return true if saved_one == new_cert
@@ -17,12 +17,12 @@ module Net
17
17
  false
18
18
  end
19
19
 
20
- def ssl_verify_cb(cert)
20
+ def ssl_cert_valid?(cert)
21
21
  identity_check = OpenSSL::SSL.verify_certificate_identity(cert, @host)
22
22
  return false unless identity_check
23
23
 
24
24
  cert_file = File.expand_path("#{@certs_path}/#{@host}.pem")
25
- return ssl_check_existing(cert, cert_file) if File.exist?(cert_file)
25
+ return ssl_known_cert_valid?(cert, cert_file) if File.exist?(cert_file)
26
26
 
27
27
  FileUtils.mkdir_p(File.expand_path(@certs_path))
28
28
  File.open(cert_file, 'wb') { |f| f.print cert.to_pem }
@@ -34,13 +34,12 @@ module Net
34
34
  ssl_context.set_params(
35
35
  verify_mode: OpenSSL::SSL::VERIFY_PEER,
36
36
  min_version: OpenSSL::SSL::TLS1_2_VERSION,
37
- ca_file: '/etc/ssl/certs/ca-certificates.crt',
38
37
  verify_hostname: true
39
38
  )
40
39
  ssl_context.verify_callback = lambda do |preverify_ok, store_context|
41
40
  return true if preverify_ok
42
41
 
43
- ssl_verify_cb store_context.current_cert
42
+ ssl_cert_valid? store_context.current_cert
44
43
  end
45
44
  ssl_context
46
45
  end
@@ -6,7 +6,7 @@ require_relative 'response'
6
6
  require_relative '../text/generic'
7
7
 
8
8
  module Net
9
- module Gemini # rubocop:disable Style/Documentation
9
+ module Gemini
10
10
  # An example client to fetch resources hosted on Gemini network.
11
11
  class Client
12
12
  attr_writer :certs_path
@@ -27,8 +27,12 @@ module Net
27
27
  res.reading_body(@ssl_socket)
28
28
  end
29
29
 
30
- def request(uri)
31
- request! uri
30
+ def request(uri, &block)
31
+ response = request! uri
32
+ yield response if block
33
+ # In any case, read it once
34
+ response.read_body
35
+ response
32
36
  rescue OpenSSL::SSL::SSLError => e
33
37
  msg = format(
34
38
  'SSLError: %<cause>s',
@@ -41,19 +45,19 @@ module Net
41
45
  finish
42
46
  end
43
47
 
44
- def fetch(uri, limit = 5)
48
+ def fetch(uri, limit = 5, &)
45
49
  raise Error, 'Too many Gemini redirects' if limit.zero?
46
50
 
47
- r = request(uri)
48
- return r unless r.status[0] == '3'
51
+ response = request(uri, &)
52
+ return response unless response.status[0] == '3'
49
53
 
50
54
  begin
51
- uri = handle_redirect(r)
55
+ uri = handle_redirect response
52
56
  rescue ArgumentError, URI::InvalidURIError
53
- return r
57
+ return response
54
58
  end
55
59
  warn "Redirect to #{uri}" if $VERBOSE
56
- fetch(uri, limit - 1)
60
+ fetch(uri, limit - 1, &)
57
61
  end
58
62
 
59
63
  private
@@ -71,6 +75,11 @@ module Net
71
75
  end
72
76
  end
73
77
 
78
+ # @param host_or_uri [String, ::URI]
79
+ # @param port [Integer, nil]
80
+ # @yield [self]
81
+ # @return [self] Returns self with no block given
82
+ # @return [Object] Returns the result of the block, if given
74
83
  def self.start(host_or_uri, port = nil, &block)
75
84
  if host_or_uri.is_a? URI::Gemini
76
85
  host = host_or_uri.host
@@ -78,16 +87,21 @@ module Net
78
87
  else
79
88
  host = host_or_uri
80
89
  end
81
- gem = Client.new(host, port)
82
- return gem unless block
90
+ client = Client.new(host, port)
91
+ return client unless block
83
92
 
84
- yield gem
93
+ yield client
85
94
  end
86
95
 
87
- def self.get_response(uri)
88
- start(uri.host, uri.port) { |gem| gem.fetch(uri) }
96
+ # @param uri [::URI]
97
+ # @yield [Response]
98
+ # @return [Response]
99
+ def self.get_response(uri, &)
100
+ start(uri.host, uri.port) { |client| client.fetch(uri, &) }
89
101
  end
90
102
 
103
+ # @param string_or_uri [String, ::URI]
104
+ # @return [String]
91
105
  def self.get(string_or_uri)
92
106
  uri = Net::Text::Generic.build_uri string_or_uri, URI::Gemini
93
107
  get_response(uri).body
@@ -32,17 +32,6 @@ module Net
32
32
  header.merge received_mime(raw_meta)
33
33
  end
34
34
 
35
- def parse_preformatted_block(line, buf)
36
- cur_block = { meta: line[3..].chomp, content: '' }
37
- while (line = buf.gets)
38
- if line.start_with?('```')
39
- @preformatted_blocks << cur_block
40
- break
41
- end
42
- cur_block[:content] += line
43
- end
44
- end
45
-
46
35
  def parse_link(line)
47
36
  m = line.strip.match(/\A=>\s*([^\s]+)(?:\s*(.+))?\z/)
48
37
  return if m.nil?
@@ -58,12 +47,16 @@ module Net
58
47
 
59
48
  def parse_body
60
49
  buf = StringIO.new(@body)
50
+ mono_block_open = false
61
51
  while (line = buf.gets)
62
- if line.start_with?('```')
63
- parse_preformatted_block(line, buf)
64
- elsif line.start_with?('=>')
65
- parse_link(line)
66
- end
52
+ mono_block_open = !mono_block_open if line.start_with?('```')
53
+
54
+ # Do not parse links inside preformatted block
55
+ next if mono_block_open
56
+
57
+ next unless line.start_with?('=>')
58
+
59
+ parse_link(line)
67
60
  end
68
61
  end
69
62
  end
@@ -19,24 +19,36 @@ module Net
19
19
  #
20
20
  class Response
21
21
  # @return [String] The Gemini response <STATUS> string.
22
- # @example '20'
22
+ # @example
23
+ # "20"
23
24
  attr_reader :status
24
25
 
25
26
  # @return [String] The Gemini response <META> message sent by the server.
26
- # @example 'text/gemini'
27
+ # @example
28
+ # "text/gemini"
27
29
  attr_reader :meta
28
30
 
29
- # @return [Hash] The Gemini response <META>.
31
+ # @return [Hash{Symbol => String, nil}] The Gemini response <META>.
32
+ # @example
33
+ # { status: '20', meta: 'text/gemini; charset=UTF-8',
34
+ # mimetype: 'text/gemini', lang: 'en',
35
+ # charset: 'utf-8', format: nil }
30
36
  attr_reader :header
31
37
 
32
38
  # The Gemini response main content as a string.
33
39
  attr_writer :body
34
40
 
35
41
  # The URI related to this response as an URI object.
42
+ # @return [::URI]
36
43
  attr_accessor :uri
37
44
 
38
- # @return [Array<String>] All links found on a Gemini response of MIME
39
- # text/gemini
45
+ # All links found on a Gemini response of MIME text/gemini
46
+ #
47
+ # Each link is a Hash with the keys `:uri` containing the link {::URI},
48
+ # and `:label` containing the link label as a {::String}, or nil if none
49
+ # was provided.
50
+ #
51
+ # @return [Array<Hash{:uri => ::URI; :label => String, nil}>]
40
52
  attr_reader :links
41
53
 
42
54
  def initialize(status = nil, meta = nil)
@@ -46,23 +58,40 @@ module Net
46
58
  @uri = nil
47
59
  @body = nil
48
60
  @links = []
49
- @preformatted_blocks = []
61
+ @socket = nil
50
62
  end
51
63
 
64
+ # Whether the current {Response} has a body of interest
65
+ # (i.e. is not an error or a redirection).
66
+ # @return [Boolean]
52
67
  def body_permitted?
53
68
  @status && @status[0] == '2'
54
69
  end
55
70
 
71
+ # Set the socket to read data through {#read_body}.
72
+ # @param sock [OpenSSL::SSL::SSLSocket]
73
+ # @return [self]
56
74
  def reading_body(sock)
57
75
  return self unless body_permitted?
58
76
 
59
- raw_body = []
60
- sock.each_line { raw_body << _1 }
61
- @body = encode_body(raw_body.join)
62
- return self unless @header[:mimetype] == 'text/gemini'
77
+ @socket = sock
78
+ self
79
+ end
80
+
81
+ # Read data from the SSL socket.
82
+ # @yield [self]
83
+ # @return [String, nil] The data read from the socket
84
+ # (aka. the Response body)
85
+ def read_body(&)
86
+ return @body unless @socket
87
+
88
+ @body = read_chunked(&)
89
+ return @body unless @header[:mimetype] == 'text/gemini'
63
90
 
64
91
  parse_body
65
- self
92
+ @body
93
+ ensure
94
+ @socket = nil
66
95
  end
67
96
 
68
97
  # Return the response body (i.e. the requested document content).
@@ -100,18 +129,27 @@ module Net
100
129
 
101
130
  private
102
131
 
103
- def encode_body(body)
104
- return body unless @header[:mimetype].start_with?('text/')
132
+ def read_chunked(&block)
133
+ raw_body = ''
134
+ is_text = @header[:mimetype].start_with?('text/')
135
+ while (chunk = @socket.read(4096))
136
+ chunk = fix_encoding chunk if is_text
137
+ yield chunk if block
138
+ raw_body += chunk
139
+ end
140
+ raw_body
141
+ end
105
142
 
143
+ def fix_encoding(data)
106
144
  if @header[:charset] && @header[:charset] != 'utf-8'
107
- # If body use another charset than utf-8, we need first to
145
+ # If data use another charset than utf-8, we need first to
108
146
  # declare the raw byte string as using this chasret
109
- body.force_encoding(@header[:charset])
147
+ data.force_encoding(@header[:charset])
110
148
  # Then we can safely try to convert it to utf-8
111
- return body.encode('utf-8')
149
+ return data.encode('utf-8')
112
150
  end
113
- # Just declare that the body uses utf-8
114
- body.force_encoding('utf-8')
151
+ # Just declare that the data uses utf-8
152
+ data.force_encoding('utf-8')
115
153
  end
116
154
  end
117
155
  end
data/lib/net/gemini.rb CHANGED
@@ -21,8 +21,7 @@ module Net
21
21
  #
22
22
  # require 'net/gemini'
23
23
  #
24
- # This will also require 'uri' so you don't need to require it
25
- # separately.
24
+ # This will also require 'uri' so you don't need to require it separately.
26
25
  #
27
26
  # The Net::Gemini methods in the following section do not persist
28
27
  # connections.
@@ -53,18 +52,47 @@ module Net
53
52
  # mimetype: 'text/gemini', lang: 'en',
54
53
  # charset: 'utf-8', format: nil }
55
54
  #
56
- # The lang, charset and format headers will only be provided in case
57
- # of `text/*` mimetype, and only if body for 2* status codes.
55
+ # The `:lang`, `:charset` and `:format` headers will only be provided in case
56
+ # of `text/*` mimetype, and {Response#body body} only for 2* status codes.
58
57
  #
59
58
  # # Body
60
59
  # puts res.body if res.body_permitted?
61
60
  # puts res.body(reflow_at: 85)
62
61
  #
62
+ # When the response is `text/gemini` mimetype, the body is parsed
63
+ # automatically to extract all its links.
64
+ #
65
+ # res.links # => [{ :uri => #<URI::Gemini gemini://…>, :label => "…" }, …]
66
+ #
67
+ # ==== Large Response
68
+ #
69
+ # You may not want to load the whole body of a large response such as images,
70
+ # videos, etc. into memory. To avoid that, you can read response body as
71
+ # chunks.
72
+ #
73
+ # f = File.new 'image.png', 'wb'
74
+ # Net::Gemini.get_response(URI('gemini://example.org/image.png')) do |res|
75
+ # res.read_body { |chunk| f.write chunk }
76
+ # end
77
+ # f.close
78
+ #
79
+ # It has to be noted that the block given to the {::get_response} method is
80
+ # executed while the socket is being read. This means the {Response} instance
81
+ # you get as block argument does not have parsed its body in case of
82
+ # a `text/gemini` mimetype, and thus its {Response#links} array will be
83
+ # empty. In any case, {::get_response} returns the {Response} instance, thus
84
+ # if you need access to these links, you must do this:
85
+ #
86
+ # uri = URI('gemini://example.org/very_long_text.gmi')
87
+ # res = Net::Gemini.get_response(uri) do |res|
88
+ # # … do something
89
+ # end
90
+ # res.links # => [{ :uri => #<URI::Gemini gemini://…>, :label => "…" }, …]
91
+ #
63
92
  # === Following Redirection
64
93
  #
65
- # The {Client#fetch} method, contrary to the {Client#request} one will try
66
- # to automatically resolves redirection, leading you to the final
67
- # destination.
94
+ # The {Client#fetch} method, contrary to the {Client#request} one will try to
95
+ # automatically resolves redirection, leading you to the final destination.
68
96
  #
69
97
  # u = URI('gemini://exemple.com/redirect')
70
98
  # res = Net::Gemini.start(u.host, u.port) do |g|
@@ -80,6 +108,12 @@ module Net
80
108
  # puts "#{res.status} - #{res.meta}" # => '20 - text/gemini;'
81
109
  # puts res.uri.to_s # => 'gemini://exemple.com/final/dest'
82
110
  #
111
+ # This fetch method is automatically called by the class methods {::get} and
112
+ # {::get_response}.
113
+ #
114
+ # res = Net::Gemini.get_response(URI('gemini://exemple.com/redirect'))
115
+ # puts "#{res.status} - #{res.meta}" # => '20 - text/gemini;'
116
+ # puts res.uri.to_s # => 'gemini://exemple.com/final/dest'
83
117
  module Gemini; end
84
118
  end
85
119
 
@@ -16,36 +16,43 @@ module Net
16
16
  ' ' * m[1].length
17
17
  end
18
18
 
19
- def self.reflow_text_line(line, mono_block_open, length)
20
- line.strip!
21
- if mono_block_open || line.start_with?('=>') || line.length < length
22
- return [line]
23
- end
24
-
19
+ def self.reflow_line(line, length)
25
20
  output = []
26
21
  prefix = reflow_line_prefix(line)
27
22
  limit_chars = ['-', '­', ' '].freeze
28
23
  while line.length > length
29
- cut_line = line[0...length]
30
- cut_index = limit_chars.map { cut_line.rindex(_1) || -1 }.max
24
+ # Detect first possible cut
25
+ cut_index = limit_chars.map { line[0...length].rindex(_1) || -1 }.max
31
26
  break if cut_index.zero? # Better do nothing for now
32
27
 
33
28
  output << line[0...cut_index]
34
- line = prefix + line[cut_index + 1..]
29
+ line = prefix + line[(cut_index + 1)..]
35
30
  end
36
31
  output << line
37
32
  end
38
33
 
34
+ def self.parse_line(line, mono_block_open, length)
35
+ if line.start_with?('```')
36
+ mono_block_open = !mono_block_open
37
+ return [mono_block_open, [line.chomp]]
38
+ end
39
+
40
+ return [mono_block_open, [line.chomp]] if mono_block_open
41
+
42
+ line.strip!
43
+ if line.start_with?('=>') || line.length < length
44
+ return [mono_block_open, [line]]
45
+ end
46
+
47
+ [mono_block_open, reflow_line(line, length)]
48
+ end
49
+
39
50
  def self.format_body(body, length)
40
51
  new_body = []
41
52
  mono_block_open = false
42
53
  body.each_line do |line|
43
- if line.start_with?('```')
44
- mono_block_open = !mono_block_open
45
- # Don't include code block toggle lines
46
- next
47
- end
48
- new_body += reflow_text_line(line, mono_block_open, length)
54
+ mono_block_open, content = parse_line line, mono_block_open, length
55
+ new_body += content
49
56
  end
50
57
  new_body.join("\n")
51
58
  end
data/lib/uri/finger.rb CHANGED
@@ -9,7 +9,7 @@ module URI # :nodoc:
9
9
  #
10
10
  # @see https://tools.ietf.org/html/rfc1288#section-2.3
11
11
  #
12
- class Finger < HTTP
12
+ class Finger < Generic
13
13
  # A Default port of 79 for URI::Finger.
14
14
  DEFAULT_PORT = 79
15
15
 
data/lib/uri/gemini.rb CHANGED
@@ -9,7 +9,7 @@ module URI # :nodoc:
9
9
  #
10
10
  # @see https://geminiprotocol.net/docs/protocol-specification.html
11
11
  #
12
- class Gemini < HTTP
12
+ class Gemini < Generic
13
13
  # A Default port of 1965 for URI::Gemini.
14
14
  DEFAULT_PORT = 1965
15
15
 
data/lib/uri/gopher.rb CHANGED
@@ -9,7 +9,7 @@ module URI # :nodoc:
9
9
  #
10
10
  # @see https://www.rfc-editor.org/rfc/rfc4266.html
11
11
  #
12
- class Gopher < HTTP
12
+ class Gopher < Generic
13
13
  # A Default port of 70 for URI::Gopher.
14
14
  DEFAULT_PORT = 70
15
15
 
data/lib/uri/nex.rb CHANGED
@@ -9,7 +9,7 @@ module URI # :nodoc:
9
9
  #
10
10
  # @see nex://nightfall.city/nex/info/specification.txt
11
11
  #
12
- class Nex < HTTP
12
+ class Nex < Generic
13
13
  # A Default port of 1900 for URI::Nex.
14
14
  DEFAULT_PORT = 1900
15
15
 
metadata CHANGED
@@ -1,16 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-net-text
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Étienne Pflieger
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email: etienne@pflieger.bzh
15
13
  executables: []
16
14
  extensions: []
@@ -56,15 +54,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
54
  requirements:
57
55
  - - ">="
58
56
  - !ruby/object:Gem::Version
59
- version: '2.7'
57
+ version: '3.3'
60
58
  required_rubygems_version: !ruby/object:Gem::Requirement
61
59
  requirements:
62
60
  - - ">="
63
61
  - !ruby/object:Gem::Version
64
62
  version: '0'
65
63
  requirements: []
66
- rubygems_version: 3.5.19
67
- signing_key:
64
+ rubygems_version: 3.6.9
68
65
  specification_version: 4
69
66
  summary: Finger, Gemini, Gopher and Nex support for Net::* and URI::*
70
67
  test_files: []