ruby-net-text 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: 95abe1fa0498d496b10d7517d9dc00fcd958091e93c75cb3f86d3274667bf245
4
+ data.tar.gz: 92855326685c71635f9a3f0c45178112925ecbd2e30027c187d473b3ddc61e07
5
+ SHA512:
6
+ metadata.gz: 7342a84385b98b84f7dc74ad546c78d58f943412da84fb247c3eff4b0affcce5ca9fa446ea527de5f32b983ee86589a57a217ad2debab57c9ede7cda0855d7d8
7
+ data.tar.gz: 17e634cc19ff48ef976db87c85155dc618a37c3e24d8dc2edd443eae647602aa6d7d9880dbb6968f289d4ad2c6a47b990ae7622abacb9d61788b023a960894bc
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Étienne Deparis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is derived from "net/http.rb".
4
+
5
+ require 'socket'
6
+ require 'openssl'
7
+ require 'fileutils'
8
+
9
+ require 'uri/gemini'
10
+ require_relative 'gemini/response'
11
+
12
+ module Net
13
+ class GeminiError < StandardError; end
14
+
15
+ # == A Gemini client API for Ruby.
16
+ #
17
+ # Net::Gemini provides a rich library which can be used to build
18
+ # Gemini user-agents.
19
+ # @see https://gemini.circumlunar.space/docs/specification.html
20
+ #
21
+ # Net::Gemini is designed to work closely with URI.
22
+ #
23
+ # == Simple Examples
24
+ #
25
+ # All examples assume you have loaded Net::Gemini with:
26
+ #
27
+ # require 'net/gemini'
28
+ #
29
+ # This will also require 'uri' so you don't need to require it
30
+ # separately.
31
+ #
32
+ # The Net::Gemini methods in the following section do not persist
33
+ # connections.
34
+ #
35
+ # === GET by URI
36
+ #
37
+ # uri = URI('gemini://example.com/index.html?count=10')
38
+ # Net::Gemini.get(uri) # => String
39
+ #
40
+ # === GET with Dynamic Parameters
41
+ #
42
+ # uri = URI('gemini://example.com/index.html')
43
+ # params = { :limit => 10, :page => 3 }
44
+ # uri.query = URI.encode_www_form(params)
45
+ #
46
+ # res = Net::Gemini.get_response(uri)
47
+ # puts res.body if res.body_permitted?
48
+ #
49
+ # === Response Data
50
+ #
51
+ # res = Net::Gemini.get_response(URI('gemini://exemple.com/home'))
52
+ #
53
+ # # Status
54
+ # puts res.status # => '20'
55
+ # puts res.meta # => 'text/gemini; charset=UTF-8; lang=en'
56
+ #
57
+ # # Headers
58
+ # puts res.header.inspect
59
+ # # => { status: '20', meta: 'text/gemini; charset=UTF-8',
60
+ # mimetype: 'text/gemini', lang: 'en',
61
+ # charset: 'utf-8', format: nil }
62
+ #
63
+ # The lang, charset and format headers will only be provided in case
64
+ # of `text/*` mimetype, and only if body for 2* status codes.
65
+ #
66
+ # # Body
67
+ # puts res.body if res.body_permitted?
68
+ # puts res.body(flowed: 85)
69
+ #
70
+ # === Following Redirection
71
+ #
72
+ # The {#fetch} method, contrary to the {#request} one will try to
73
+ # automatically resolves redirection, leading you to the final
74
+ # destination.
75
+ #
76
+ # u = URI('gemini://exemple.com/redirect')
77
+ # res = Net::Gemini.start(u.host, u.port) do |g|
78
+ # g.request(u)
79
+ # end
80
+ # puts "#{res.status} - #{res.meta}" # => '30 final/dest'
81
+ # puts res.uri.to_s # => 'gemini://exemple.com/redirect'
82
+ #
83
+ # u = URI('gemini://exemple.com/redirect')
84
+ # res = Net::Gemini.start(u.host, u.port) do |g|
85
+ # g.fetch(u)
86
+ # end
87
+ # puts "#{res.status} - #{res.meta}" # => '20 - text/gemini;'
88
+ # puts res.uri.to_s # => 'gemini://exemple.com/final/dest'
89
+ #
90
+ class Gemini
91
+ attr_writer :certs_path
92
+
93
+ def initialize(host, port)
94
+ @host = host
95
+ @port = port
96
+ @certs_path = '~/.cache/gemini/certs'
97
+ end
98
+
99
+ def request(uri)
100
+ init_sockets
101
+ @ssl_socket.puts "#{uri}\r\n"
102
+ r = GeminiResponse.read_new(@ssl_socket)
103
+ r.uri = uri
104
+ r.reading_body(@ssl_socket)
105
+ ensure
106
+ # Stop remaining connection, even if they should be already cut
107
+ # by the server
108
+ finish
109
+ end
110
+
111
+ def fetch(uri, limit = 5)
112
+ raise GeminiError, 'Too many Gemini redirects' if limit.zero?
113
+ r = request(uri)
114
+ return r unless r.status[0] == '3'
115
+ old_url = uri.to_s
116
+ begin
117
+ new_uri = URI(r.meta)
118
+ uri.merge!(new_uri)
119
+ rescue ArgumentError, URI::InvalidURIError
120
+ return r
121
+ end
122
+ raise GeminiError, "Redirect loop on #{uri}" if uri.to_s == old_url
123
+ warn "Redirect to #{uri}" if $VERBOSE
124
+ fetch(uri, limit - 1)
125
+ end
126
+
127
+ class << self
128
+ def start(host_or_uri, port = nil)
129
+ if host_or_uri.is_a? URI::Gemini
130
+ host = host_or_uri.host
131
+ port = host_or_uri.port
132
+ else
133
+ host = host_or_uri
134
+ end
135
+ gem = new(host, port)
136
+ return yield(gem) if block_given?
137
+ gem
138
+ end
139
+
140
+ def get_response(uri)
141
+ start(uri.host, uri.port) { |gem| gem.fetch(uri) }
142
+ end
143
+
144
+ def get(uri)
145
+ get_response(uri).body
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def ssl_check_existing(new_cert, cert_file)
152
+ raw = File.read cert_file
153
+ saved_one = OpenSSL::X509::Certificate.new raw
154
+ return true if saved_one == new_cert
155
+ # TODO: offer some kind of recuperation
156
+ warn "#{cert_file} does not match the current host cert!"
157
+ false
158
+ end
159
+
160
+ def ssl_verify_cb(cert)
161
+ domain = cert.subject.to_s.sub(/^\/CN=/, '')
162
+ return false if domain != @host
163
+ cert_file = File.expand_path("#{@certs_path}/#{domain}.pem")
164
+ return ssl_check_existing(cert, cert_file) if File.exist?(cert_file)
165
+ FileUtils.mkdir_p(File.expand_path(@certs_path))
166
+ File.open(cert_file, 'wb') { |f| f.print cert.to_pem }
167
+ true
168
+ end
169
+
170
+ def ssl_context
171
+ ssl_context = OpenSSL::SSL::SSLContext.new
172
+ ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
173
+ ssl_context.min_version = OpenSSL::SSL::TLS1_2_VERSION
174
+ ssl_context.verify_hostname = true
175
+ ssl_context.ca_file = '/etc/ssl/certs/ca-certificates.crt'
176
+ ssl_context.verify_callback = lambda do |preverify_ok, store_context|
177
+ return true if preverify_ok
178
+ ssl_verify_cb store_context.current_cert
179
+ end
180
+ ssl_context
181
+ end
182
+
183
+ def init_sockets
184
+ @socket = TCPSocket.new(@host, @port)
185
+ @ssl_socket = OpenSSL::SSL::SSLSocket.new(@socket, ssl_context)
186
+ # Close underlying TCP socket with SSL socket
187
+ @ssl_socket.sync_close = true
188
+ @ssl_socket.hostname = @host # SNI
189
+ @ssl_socket.connect
190
+ end
191
+
192
+ # Closes the SSL and TCP connections.
193
+ def finish
194
+ @ssl_socket.close
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'stringio'
5
+
6
+ require_relative 'gmi_parser'
7
+ require_relative 'reflow_text'
8
+
9
+ module Net
10
+ class GeminiBadResponse < StandardError; end
11
+
12
+ #
13
+ # The syntax of Gemini Responses are defined in the Gemini
14
+ # specification[1], section 3.
15
+ #
16
+ # [1] https://gemini.circumlunar.space/docs/specification.html
17
+ #
18
+ class GeminiResponse
19
+ # The Gemini response <STATUS> string.
20
+ #
21
+ # For example, '20'.
22
+ attr_reader :status
23
+
24
+ # The Gemini response <META> message sent by the server as a string.
25
+ #
26
+ # For example, 'text/gemini'.
27
+ attr_reader :meta
28
+
29
+ # The Gemini response <META> as a qualified Hash.
30
+ attr_reader :header
31
+
32
+ # The Gemini response main content as a string.
33
+ attr_writer :body
34
+
35
+ # The URI related to this response as an URI object.
36
+ attr_accessor :uri
37
+
38
+ # All links found on a Gemini response of MIME text/gemini, as an
39
+ # array.
40
+ attr_reader :links
41
+
42
+ def initialize(status = nil, meta = nil)
43
+ @status = status
44
+ @meta = meta
45
+ @header = parse_meta
46
+ @uri = nil
47
+ @body = nil
48
+ @links = []
49
+ @preformatted_blocks = []
50
+ end
51
+
52
+ def body_permitted?
53
+ @status && @status[0] == '2'
54
+ end
55
+
56
+ def reading_body(sock)
57
+ return self unless body_permitted?
58
+ raw_body = []
59
+ while (line = sock.gets)
60
+ raw_body << line
61
+ end
62
+ @body = encode_body(raw_body.join)
63
+ return self unless @header[:mimetype] == 'text/gemini'
64
+ parse_body
65
+ self
66
+ end
67
+
68
+ def body(flowed: nil)
69
+ return '' if @body.nil? # Maybe not ready?
70
+ return @body if flowed.nil? || @header[:format] == 'fixed'
71
+ reformat_body(flowed)
72
+ end
73
+
74
+ class << self
75
+ def read_new(sock)
76
+ code, msg = read_status_line(sock)
77
+ new(code, msg)
78
+ end
79
+
80
+ private
81
+
82
+ def read_status_line(sock)
83
+ # Read up to 1027 bytes:
84
+ # - 3 bytes for code and space separator
85
+ # - 1024 bytes max for the message
86
+ str = sock.gets($INPUT_RECORD_SEPARATOR, 1027)
87
+ m = /\A([1-6]\d) (.*)\r\n\z/.match(str)
88
+ raise GeminiBadResponse, "wrong status line: #{str.dump}" if m.nil?
89
+ m.captures
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def encode_body(body)
96
+ return body unless @header[:mimetype].start_with?('text/')
97
+ if @header[:charset] && @header[:charset] != 'utf-8'
98
+ # If body use another charset than utf-8, we need first to
99
+ # declare the raw byte string as using this chasret
100
+ body.force_encoding(@header[:charset])
101
+ # Then we can safely try to convert it to utf-8
102
+ return body.encode('utf-8')
103
+ end
104
+ # Just declare that the body uses utf-8
105
+ body.force_encoding('utf-8')
106
+ end
107
+
108
+ include ::Gemini::GmiParser
109
+ include ::Gemini::ReflowText
110
+ end
111
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module URI # :nodoc:
6
+ #
7
+ # The syntax of Finger URIs is defined in the Finger specification,
8
+ # section 2.3.
9
+ #
10
+ # @see https://tools.ietf.org/html/rfc1288#section-2.3
11
+ #
12
+ class Finger < HTTP
13
+ # A Default port of 79 for URI::Finger.
14
+ DEFAULT_PORT = 79
15
+
16
+ # An Array of the available components for URI::Finger.
17
+ COMPONENT = [:scheme, :userinfo, :host, :port].freeze
18
+ end
19
+
20
+ @@schemes['FINGER'] = Finger
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module URI # :nodoc:
6
+ #
7
+ # The syntax of Gemini URIs is defined in the Gemini specification,
8
+ # section 1.2.
9
+ #
10
+ # @see https://gemini.circumlunar.space/docs/specification.html
11
+ #
12
+ class Gemini < HTTP
13
+ # A Default port of 1965 for URI::Gemini.
14
+ DEFAULT_PORT = 1965
15
+
16
+ # An Array of the available components for URI::Gemini.
17
+ COMPONENT = [:scheme, :host, :port,
18
+ :path, :query, :fragment].freeze
19
+ end
20
+
21
+ @@schemes['GEMINI'] = Gemini
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module URI # :nodoc:
6
+ #
7
+ # The syntax of Gopher URIs is defined in the Gopher URI Scheme
8
+ # specification.
9
+ #
10
+ # @see https://www.rfc-editor.org/rfc/rfc4266.html
11
+ #
12
+ class Gopher < HTTP
13
+ # A Default port of 70 for URI::Gopher.
14
+ DEFAULT_PORT = 70
15
+
16
+ # An Array of the available components for URI::Gopher.
17
+ COMPONENT = [:scheme, :host, :port, :path].freeze
18
+ end
19
+
20
+ @@schemes['GOPHER'] = Gopher
21
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-net-text
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-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: etienne@depar.is
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE
20
+ - lib/net/gemini.rb
21
+ - lib/net/gemini/response.rb
22
+ - lib/uri/finger.rb
23
+ - lib/uri/gemini.rb
24
+ - lib/uri/gopher.rb
25
+ homepage: https://git.umaneti.net/ruby-net-text/
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '2.7'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.1.4
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Gemini, Gopher, and Finger support for Net::* and URI::*
48
+ test_files: []