ruby-net-text 0.0.8 → 0.0.9
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 +4 -4
- data/README.md +35 -7
- data/lib/net/finger.rb +2 -4
- data/lib/net/gemini/client/ssl.rb +60 -0
- data/lib/net/gemini/client.rb +87 -0
- data/lib/net/gemini/error.rb +9 -0
- data/lib/net/gemini/request.rb +39 -36
- data/lib/net/gemini/response/parser.rb +60 -0
- data/lib/net/gemini/response.rb +90 -84
- data/lib/net/gemini.rb +6 -88
- data/lib/net/gopher.rb +2 -4
- data/lib/net/nex.rb +37 -0
- data/lib/net/text/generic.rb +26 -0
- data/lib/net/text/reflow.rb +58 -0
- data/lib/uri/nex.rb +26 -0
- metadata +20 -9
- data/lib/net/gemini/gmi_parser.rb +0 -57
- data/lib/net/gemini/reflow_text.rb +0 -62
- data/lib/net/gemini/ssl.rb +0 -53
- data/lib/net/generic.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 866ba253d1dbbc5b2e9a8eba1f6bf4813c6f3b163ce3278d36c4932be80c3714
|
4
|
+
data.tar.gz: 16e0059f252a8d88c8c04667e1b779e65eadae7c1c6da17aba4487fdac459070
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4643771216d1ee0c494a5e368037fbfcfcf303a290dfa2260af3cb9125cbdbeaad6d4fc7ee3836abb914e5c5f731d4f351fb1e4e19193751b07bfd31a0385abe
|
7
|
+
data.tar.gz: 1625097ffa76cc274460e64987fca75cb43db9963d4401e434a796b4aa7effdbbbda9e787001597183ce4d8e91db15f0c0c8f5f8945b725485aef218b314ab4d
|
data/README.md
CHANGED
@@ -1,19 +1,47 @@
|
|
1
|
-
# Gemini, Gopher
|
1
|
+
# Finger, Gemini, Gopher and Nex support for Net::* and URI::*
|
2
2
|
|
3
3
|
[](https://liberapay.com/milouse/donate)
|
4
4
|
[](https://flattr.com/@milouse)
|
5
5
|
[](https://paypal.me/milouse)
|
6
6
|
|
7
7
|
[](https://rubygems.org/gems/ruby-net-text)
|
8
|
-
[](https://www.rubydoc.info/gems/ruby-net-text/
|
8
|
+
[](https://www.rubydoc.info/gems/ruby-net-text/)
|
9
9
|
|
10
10
|
This project aims to add connectors to well known internet text protocols
|
11
11
|
through the standard `Net::*` and `URI::*` ruby module namespaces.
|
12
12
|
|
13
|
+
## News
|
14
|
+
|
15
|
+
### Version 0.0.9 gemini breaking changes
|
16
|
+
|
17
|
+
This new version changes the Gemini namespace. Everything is now under the
|
18
|
+
same `Net::Gemini` namespace. If you just used this gem as per the
|
19
|
+
documentation, nothing changes for you. However, if you were using some hidden
|
20
|
+
part of the Gemini API, you will probably have to make some changes.
|
21
|
+
|
22
|
+
Here are all the changes:
|
23
|
+
|
24
|
+
| Old names | New names |
|
25
|
+
|------------------------|--------------------------------------------------------------------------|
|
26
|
+
| Net::GeminiRequest | Net::Gemini::Request (still 'net/gemini/request') |
|
27
|
+
| Net::GeminiBadRequest | Net::Gemini::BadRequest (require 'net/gemini/error') |
|
28
|
+
| Net::GeminiResponse | Net::Gemini::Response (still 'net/gemini/response') |
|
29
|
+
| Net::GeminiBadResponse | Net::Gemini::BadResponse (require 'net/gemini/error') |
|
30
|
+
| Net::GeminiError | Net::Gemini::Error (require 'net/gemini/error') |
|
31
|
+
| Net::Gemini.new | Net::Gemini::Client.new (directly required as part of 'net/gemini') |
|
32
|
+
| Gemini::ReflowText | Net::Text::Reflow (no more expected to be included, but directly called) |
|
33
|
+
| Gemini::GmiParser | - (directly integrated into Net::Gemini::Response) |
|
34
|
+
| Gemini::SSL | - (directly integrated into Net::Gemini::Client) |
|
35
|
+
|
13
36
|
## Documentation
|
14
37
|
|
15
38
|
The code is self-documented and you can browse it on rubydoc.info:
|
16
39
|
|
40
|
+
### Finger
|
41
|
+
|
42
|
+
- [URI::Finger](https://www.rubydoc.info/gems/ruby-net-text/URI/Finger)
|
43
|
+
- [Net::Finger](https://www.rubydoc.info/gems/ruby-net-text/Net/Finger)
|
44
|
+
|
17
45
|
### Gemini
|
18
46
|
|
19
47
|
- [URI::Gemini](https://www.rubydoc.info/gems/ruby-net-text/URI/Gemini)
|
@@ -24,16 +52,16 @@ The code is self-documented and you can browse it on rubydoc.info:
|
|
24
52
|
- [URI::Gopher](https://www.rubydoc.info/gems/ruby-net-text/URI/Gopher)
|
25
53
|
- [Net::Gopher](https://www.rubydoc.info/gems/ruby-net-text/Net/Gopher)
|
26
54
|
|
27
|
-
###
|
55
|
+
### Nex
|
28
56
|
|
29
|
-
- [URI::
|
30
|
-
- [Net::
|
57
|
+
- [URI::Nex](https://www.rubydoc.info/gems/ruby-net-text/URI/Nex)
|
58
|
+
- [Net::Nex](https://www.rubydoc.info/gems/ruby-net-text/Net/Nex)
|
31
59
|
|
32
60
|
## Helpers
|
33
61
|
|
34
62
|
This repository also includes 2 little helpers:
|
35
63
|
|
36
|
-
- `bin/heraut`: a toy client for Gemini, Gopher and
|
37
|
-
it will output the remote file.
|
64
|
+
- `bin/heraut`: a toy client for Finger, Gemini, Gopher and Nex. Give it a URI
|
65
|
+
and it will output the remote file.
|
38
66
|
- `bin/test_thread.rb`: a toy performance test script to run against a Gemini
|
39
67
|
server
|
data/lib/net/finger.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../uri/finger'
|
4
|
-
require_relative 'generic'
|
4
|
+
require_relative 'text/generic'
|
5
5
|
|
6
6
|
module Net
|
7
7
|
# == A Finger client API for Ruby.
|
@@ -28,9 +28,7 @@ module Net
|
|
28
28
|
# uri = URI('finger://skyjake.fi/jaakko')
|
29
29
|
# Net::Finger.get(uri) # => String
|
30
30
|
#
|
31
|
-
class Finger
|
32
|
-
extend TextGeneric
|
33
|
-
|
31
|
+
class Finger < Text::Generic
|
34
32
|
def self.get(string_or_uri)
|
35
33
|
uri = build_uri string_or_uri, URI::Finger
|
36
34
|
request uri, uri.name.to_s
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Gemini
|
5
|
+
# Reopen Client class to add specific private method to handle SSL
|
6
|
+
# connection
|
7
|
+
class Client
|
8
|
+
private
|
9
|
+
|
10
|
+
def ssl_check_existing(new_cert, cert_file)
|
11
|
+
raw = File.read cert_file
|
12
|
+
saved_one = OpenSSL::X509::Certificate.new raw
|
13
|
+
return true if saved_one == new_cert
|
14
|
+
|
15
|
+
# TODO: offer some kind of recuperation
|
16
|
+
warn "#{cert_file} does not match the current host cert!"
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def ssl_verify_cb(cert)
|
21
|
+
identity_check = OpenSSL::SSL.verify_certificate_identity(cert, @host)
|
22
|
+
return false unless identity_check
|
23
|
+
|
24
|
+
cert_file = File.expand_path("#{@certs_path}/#{@host}.pem")
|
25
|
+
return ssl_check_existing(cert, cert_file) if File.exist?(cert_file)
|
26
|
+
FileUtils.mkdir_p(File.expand_path(@certs_path))
|
27
|
+
File.open(cert_file, 'wb') { |f| f.print cert.to_pem }
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def ssl_context
|
32
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
33
|
+
ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
34
|
+
ssl_context.min_version = OpenSSL::SSL::TLS1_2_VERSION
|
35
|
+
ssl_context.verify_hostname = true
|
36
|
+
ssl_context.ca_file = '/etc/ssl/certs/ca-certificates.crt'
|
37
|
+
ssl_context.verify_callback = lambda do |preverify_ok, store_context|
|
38
|
+
return true if preverify_ok
|
39
|
+
|
40
|
+
ssl_verify_cb store_context.current_cert
|
41
|
+
end
|
42
|
+
ssl_context
|
43
|
+
end
|
44
|
+
|
45
|
+
def init_sockets
|
46
|
+
socket = TCPSocket.new(@host, @port)
|
47
|
+
@ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
48
|
+
# Close underlying TCP socket with SSL socket
|
49
|
+
@ssl_socket.sync_close = true
|
50
|
+
@ssl_socket.hostname = @host # SNI
|
51
|
+
@ssl_socket.connect
|
52
|
+
end
|
53
|
+
|
54
|
+
# Closes the SSL and TCP connections.
|
55
|
+
def finish
|
56
|
+
@ssl_socket.close
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'request'
|
5
|
+
require_relative 'response'
|
6
|
+
|
7
|
+
module Net
|
8
|
+
module Gemini
|
9
|
+
class Client
|
10
|
+
attr_writer :certs_path
|
11
|
+
|
12
|
+
def initialize(host, port)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
@certs_path = '~/.cache/gemini/certs'
|
16
|
+
end
|
17
|
+
|
18
|
+
def request(uri)
|
19
|
+
init_sockets
|
20
|
+
req = Request.new uri
|
21
|
+
req.write @ssl_socket
|
22
|
+
res = Response.read_new(@ssl_socket)
|
23
|
+
res.uri = uri
|
24
|
+
res.reading_body(@ssl_socket)
|
25
|
+
rescue OpenSSL::SSL::SSLError => e
|
26
|
+
msg = format(
|
27
|
+
'SSLError: %<cause>s',
|
28
|
+
cause: e.message.sub(/.*state=error: (.+)\Z/, '\1')
|
29
|
+
)
|
30
|
+
Response.new('59', msg)
|
31
|
+
ensure
|
32
|
+
# Stop remaining connection, even if they should be already cut
|
33
|
+
# by the server
|
34
|
+
finish
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch(uri, limit = 5)
|
38
|
+
raise Error, 'Too many Gemini redirects' if limit.zero?
|
39
|
+
r = request(uri)
|
40
|
+
return r unless r.status[0] == '3'
|
41
|
+
begin
|
42
|
+
uri = handle_redirect(r)
|
43
|
+
rescue ArgumentError, URI::InvalidURIError
|
44
|
+
return r
|
45
|
+
end
|
46
|
+
warn "Redirect to #{uri}" if $VERBOSE
|
47
|
+
fetch(uri, limit - 1)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def handle_redirect(response)
|
53
|
+
uri = response.uri
|
54
|
+
old_url = uri.to_s
|
55
|
+
new_uri = URI(response.meta)
|
56
|
+
uri.merge!(new_uri)
|
57
|
+
raise Error, "Redirect loop on #{uri}" if uri.to_s == old_url
|
58
|
+
@host = uri.host
|
59
|
+
@port = uri.port
|
60
|
+
uri
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.start(host_or_uri, port = nil, &block)
|
65
|
+
if host_or_uri.is_a? URI::Gemini
|
66
|
+
host = host_or_uri.host
|
67
|
+
port = host_or_uri.port
|
68
|
+
else
|
69
|
+
host = host_or_uri
|
70
|
+
end
|
71
|
+
gem = Client.new(host, port)
|
72
|
+
return gem unless block
|
73
|
+
|
74
|
+
yield gem
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.get_response(uri)
|
78
|
+
start(uri.host, uri.port) { |gem| gem.fetch(uri) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.get(uri)
|
82
|
+
get_response(uri).body
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
require_relative 'client/ssl'
|
data/lib/net/gemini/request.rb
CHANGED
@@ -2,48 +2,51 @@
|
|
2
2
|
|
3
3
|
require 'English'
|
4
4
|
|
5
|
-
|
5
|
+
require_relative '../../uri/gemini'
|
6
|
+
require_relative 'error'
|
6
7
|
|
7
8
|
module Net
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
9
|
+
module Gemini
|
10
|
+
class BadRequest < Error; end
|
11
|
+
|
12
|
+
#
|
13
|
+
# The syntax of Gemini Requests are defined in the Gemini
|
14
|
+
# specification, section 2.
|
15
|
+
#
|
16
|
+
# @see https://gemini.circumlunar.space/docs/specification.html
|
17
|
+
#
|
18
|
+
class Request
|
19
|
+
attr_reader :uri
|
20
|
+
|
21
|
+
def initialize(uri_or_str)
|
22
|
+
# In any case, make some sanity check over this uri-like think
|
23
|
+
url = uri_or_str.to_s
|
24
|
+
if url.length > 1024
|
25
|
+
raise BadRequest, "Request too long: #{url.dump}"
|
26
|
+
end
|
27
|
+
@uri = URI(url)
|
28
|
+
return if uri.is_a? URI::Gemini
|
29
|
+
raise BadRequest, "Not a Gemini URI: #{url.dump}"
|
24
30
|
end
|
25
|
-
@uri = URI(url)
|
26
|
-
return if uri.is_a? URI::Gemini
|
27
|
-
raise GeminiBadRequest, "Not a Gemini URI: #{url.dump}"
|
28
|
-
end
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
def path
|
33
|
+
@uri.path
|
34
|
+
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
def write(sock)
|
37
|
+
sock.puts "#{@uri}\r\n"
|
38
|
+
end
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
40
|
+
class << self
|
41
|
+
def read_new(sock)
|
42
|
+
# Read up to 1026 bytes:
|
43
|
+
# - 1024 bytes max for the URL
|
44
|
+
# - 2 bytes for <CR><LF>
|
45
|
+
str = sock.gets($INPUT_RECORD_SEPARATOR, 1026)
|
46
|
+
m = /\A(.*)\r\n\z/.match(str)
|
47
|
+
raise BadRequest, "Malformed request: #{str&.dump}" if m.nil?
|
48
|
+
new(m[1])
|
49
|
+
end
|
47
50
|
end
|
48
51
|
end
|
49
52
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Gemini
|
5
|
+
# Reopen Response class to add specific private method to parse
|
6
|
+
# text/gemini documents.
|
7
|
+
class Response
|
8
|
+
private
|
9
|
+
|
10
|
+
def parse_meta
|
11
|
+
header = { status: @status, meta: @meta, mimetype: nil }
|
12
|
+
return header unless body_permitted?
|
13
|
+
mime = { lang: nil, charset: 'utf-8', format: nil }
|
14
|
+
raw_meta = meta.split(';').map(&:strip)
|
15
|
+
header[:mimetype] = raw_meta.shift
|
16
|
+
return header unless raw_meta.any?
|
17
|
+
raw_meta.map { |m| m.split('=') }.each do |opt|
|
18
|
+
key = opt[0].downcase.to_sym
|
19
|
+
next unless mime.has_key? key
|
20
|
+
mime[key] = opt[1].downcase
|
21
|
+
end
|
22
|
+
header.merge(mime)
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_preformatted_block(line, buf)
|
26
|
+
cur_block = { meta: line[3..].chomp, content: '' }
|
27
|
+
while (line = buf.gets)
|
28
|
+
if line.start_with?('```')
|
29
|
+
@preformatted_blocks << cur_block
|
30
|
+
break
|
31
|
+
end
|
32
|
+
cur_block[:content] += line
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_link(line)
|
37
|
+
m = line.strip.match(/\A=>\s*([^\s]+)(?:\s*(.+))?\z/)
|
38
|
+
return if m.nil?
|
39
|
+
begin
|
40
|
+
uri = URI(m[1])
|
41
|
+
rescue URI::InvalidURIError
|
42
|
+
return
|
43
|
+
end
|
44
|
+
uri = @uri.merge(uri) if @uri && uri.is_a?(URI::Generic)
|
45
|
+
@links << { uri: uri, label: m[2]&.chomp }
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_body
|
49
|
+
buf = StringIO.new(@body)
|
50
|
+
while (line = buf.gets)
|
51
|
+
if line.start_with?('```')
|
52
|
+
parse_preformatted_block(line, buf)
|
53
|
+
elsif line.start_with?('=>')
|
54
|
+
parse_link(line)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/net/gemini/response.rb
CHANGED
@@ -3,103 +3,109 @@
|
|
3
3
|
require 'English'
|
4
4
|
require 'stringio'
|
5
5
|
|
6
|
-
require_relative '
|
7
|
-
require_relative '
|
6
|
+
require_relative 'error'
|
7
|
+
require_relative '../text/reflow'
|
8
8
|
|
9
9
|
module Net
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
# The syntax of Gemini Responses are defined in the Gemini
|
14
|
-
# specification, section 3.
|
15
|
-
#
|
16
|
-
# @see https://gemini.circumlunar.space/docs/specification.html
|
17
|
-
#
|
18
|
-
class GeminiResponse
|
19
|
-
# The Gemini response <STATUS> string.
|
10
|
+
module Gemini
|
20
11
|
#
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
# The Gemini response <META> message sent by the server as a string.
|
12
|
+
# The syntax of Gemini Responses are defined in the Gemini
|
13
|
+
# specification, section 3.
|
25
14
|
#
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
@
|
50
|
-
|
15
|
+
# @see https://gemini.circumlunar.space/docs/specification.html
|
16
|
+
#
|
17
|
+
# See {Net::Gemini} documentation to see how to interract with a
|
18
|
+
# Response.
|
19
|
+
#
|
20
|
+
class Response
|
21
|
+
# @return [String] The Gemini response <STATUS> string.
|
22
|
+
# @example '20'
|
23
|
+
attr_reader :status
|
24
|
+
|
25
|
+
# @return [String] The Gemini response <META> message sent by the server.
|
26
|
+
# @example 'text/gemini'
|
27
|
+
attr_reader :meta
|
28
|
+
|
29
|
+
# @return [Hash] The Gemini response <META>.
|
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
|
+
# @return [Array<String>] All links found on a Gemini response of MIME
|
39
|
+
# text/gemini
|
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
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
def body_permitted?
|
53
|
+
@status && @status[0] == '2'
|
54
|
+
end
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
61
66
|
end
|
62
|
-
@body = encode_body(raw_body.join)
|
63
|
-
return self unless @header[:mimetype] == 'text/gemini'
|
64
|
-
parse_body
|
65
|
-
self
|
66
|
-
end
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
68
|
+
# Return the response body (i.e. the requested document content).
|
69
|
+
#
|
70
|
+
# @param reflow_at [Integer] The column at which body content must be
|
71
|
+
# reflowed. Default is -1, which means "do not reflow".
|
72
|
+
# @return [String] the body content
|
73
|
+
def body(reflow_at: -1)
|
74
|
+
return '' if @body.nil? # Maybe not ready?
|
75
|
+
return @body if reflow_at < 0 || @header[:format] == 'fixed'
|
73
76
|
|
74
|
-
|
75
|
-
def read_new(sock)
|
76
|
-
# Read up to 1029 bytes:
|
77
|
-
# - 3 bytes for code and space separator
|
78
|
-
# - 1024 bytes max for the message
|
79
|
-
# - 2 bytes for <CR><LF>
|
80
|
-
str = sock.gets($INPUT_RECORD_SEPARATOR, 1029)
|
81
|
-
m = /\A([1-6]\d) (.*)\r\n\z/.match(str)
|
82
|
-
raise GeminiBadResponse, "wrong status line: #{str.dump}" if m.nil?
|
83
|
-
new(*m.captures)
|
77
|
+
Net::Text::Reflow.format_body(@body, reflow_at)
|
84
78
|
end
|
85
|
-
end
|
86
79
|
|
87
|
-
|
80
|
+
class << self
|
81
|
+
def read_new(sock)
|
82
|
+
# Read up to 1029 bytes:
|
83
|
+
# - 3 bytes for code and space separator
|
84
|
+
# - 1024 bytes max for the message
|
85
|
+
# - 2 bytes for <CR><LF>
|
86
|
+
str = sock.gets($INPUT_RECORD_SEPARATOR, 1029)
|
87
|
+
m = /\A([1-6]\d) (.*)\r\n\z/.match(str)
|
88
|
+
raise BadResponse, "wrong status line: #{str.dump}" if m.nil?
|
89
|
+
new(*m.captures)
|
90
|
+
end
|
91
|
+
end
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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')
|
97
106
|
end
|
98
|
-
# Just declare that the body uses utf-8
|
99
|
-
body.force_encoding('utf-8')
|
100
107
|
end
|
101
|
-
|
102
|
-
include ::Gemini::GmiParser
|
103
|
-
include ::Gemini::ReflowText
|
104
108
|
end
|
105
109
|
end
|
110
|
+
|
111
|
+
require_relative 'response/parser'
|
data/lib/net/gemini.rb
CHANGED
@@ -6,14 +6,7 @@ require 'socket'
|
|
6
6
|
require 'openssl'
|
7
7
|
require 'fileutils'
|
8
8
|
|
9
|
-
require 'uri/gemini'
|
10
|
-
require_relative 'gemini/request'
|
11
|
-
require_relative 'gemini/response'
|
12
|
-
require_relative 'gemini/ssl'
|
13
|
-
|
14
9
|
module Net
|
15
|
-
class GeminiError < StandardError; end
|
16
|
-
|
17
10
|
# == A Gemini client API for Ruby.
|
18
11
|
#
|
19
12
|
# Net::Gemini provides a library which can be used to build Gemini
|
@@ -66,12 +59,12 @@ module Net
|
|
66
59
|
#
|
67
60
|
# # Body
|
68
61
|
# puts res.body if res.body_permitted?
|
69
|
-
# puts res.body(
|
62
|
+
# puts res.body(reflow_at: 85)
|
70
63
|
#
|
71
64
|
# === Following Redirection
|
72
65
|
#
|
73
|
-
# The {#fetch} method, contrary to the {#request} one will try
|
74
|
-
# automatically resolves redirection, leading you to the final
|
66
|
+
# The {Client#fetch} method, contrary to the {Client#request} one will try
|
67
|
+
# to automatically resolves redirection, leading you to the final
|
75
68
|
# destination.
|
76
69
|
#
|
77
70
|
# u = URI('gemini://exemple.com/redirect')
|
@@ -88,82 +81,7 @@ module Net
|
|
88
81
|
# puts "#{res.status} - #{res.meta}" # => '20 - text/gemini;'
|
89
82
|
# puts res.uri.to_s # => 'gemini://exemple.com/final/dest'
|
90
83
|
#
|
91
|
-
|
92
|
-
attr_writer :certs_path
|
93
|
-
|
94
|
-
def initialize(host, port)
|
95
|
-
@host = host
|
96
|
-
@port = port
|
97
|
-
@certs_path = '~/.cache/gemini/certs'
|
98
|
-
end
|
99
|
-
|
100
|
-
def request(uri)
|
101
|
-
init_sockets
|
102
|
-
req = GeminiRequest.new uri
|
103
|
-
req.write @ssl_socket
|
104
|
-
res = GeminiResponse.read_new(@ssl_socket)
|
105
|
-
res.uri = uri
|
106
|
-
res.reading_body(@ssl_socket)
|
107
|
-
rescue OpenSSL::SSL::SSLError => e
|
108
|
-
msg = format(
|
109
|
-
'SSLError: %<cause>s',
|
110
|
-
cause: e.message.sub(/.*state=error: (.+)\Z/, '\1')
|
111
|
-
)
|
112
|
-
GeminiResponse.new('59', msg)
|
113
|
-
ensure
|
114
|
-
# Stop remaining connection, even if they should be already cut
|
115
|
-
# by the server
|
116
|
-
finish
|
117
|
-
end
|
118
|
-
|
119
|
-
def fetch(uri, limit = 5)
|
120
|
-
raise GeminiError, 'Too many Gemini redirects' if limit.zero?
|
121
|
-
r = request(uri)
|
122
|
-
return r unless r.status[0] == '3'
|
123
|
-
begin
|
124
|
-
uri = handle_redirect(r)
|
125
|
-
rescue ArgumentError, URI::InvalidURIError
|
126
|
-
return r
|
127
|
-
end
|
128
|
-
warn "Redirect to #{uri}" if $VERBOSE
|
129
|
-
fetch(uri, limit - 1)
|
130
|
-
end
|
131
|
-
|
132
|
-
class << self
|
133
|
-
def start(host_or_uri, port = nil)
|
134
|
-
if host_or_uri.is_a? URI::Gemini
|
135
|
-
host = host_or_uri.host
|
136
|
-
port = host_or_uri.port
|
137
|
-
else
|
138
|
-
host = host_or_uri
|
139
|
-
end
|
140
|
-
gem = new(host, port)
|
141
|
-
return yield(gem) if block_given?
|
142
|
-
gem
|
143
|
-
end
|
144
|
-
|
145
|
-
def get_response(uri)
|
146
|
-
start(uri.host, uri.port) { |gem| gem.fetch(uri) }
|
147
|
-
end
|
148
|
-
|
149
|
-
def get(uri)
|
150
|
-
get_response(uri).body
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
private
|
155
|
-
|
156
|
-
def handle_redirect(response)
|
157
|
-
uri = response.uri
|
158
|
-
old_url = uri.to_s
|
159
|
-
new_uri = URI(response.meta)
|
160
|
-
uri.merge!(new_uri)
|
161
|
-
raise GeminiError, "Redirect loop on #{uri}" if uri.to_s == old_url
|
162
|
-
@host = uri.host
|
163
|
-
@port = uri.port
|
164
|
-
uri
|
165
|
-
end
|
166
|
-
|
167
|
-
include ::Gemini::SSL
|
168
|
-
end
|
84
|
+
module Gemini; end
|
169
85
|
end
|
86
|
+
|
87
|
+
require_relative 'gemini/client'
|
data/lib/net/gopher.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../uri/gopher'
|
4
|
-
require_relative 'generic'
|
4
|
+
require_relative 'text/generic'
|
5
5
|
|
6
6
|
module Net
|
7
7
|
# == A Gopher client API for Ruby.
|
@@ -28,9 +28,7 @@ module Net
|
|
28
28
|
# uri = URI('gopher://thelambdalab.xyz/1/projects/elpher/')
|
29
29
|
# Net::Gopher.get(uri) # => String
|
30
30
|
#
|
31
|
-
class Gopher
|
32
|
-
extend TextGeneric
|
33
|
-
|
31
|
+
class Gopher < Text::Generic
|
34
32
|
def self.get(string_or_uri)
|
35
33
|
uri = build_uri string_or_uri, URI::Gopher
|
36
34
|
request uri, "#{uri.selector}\r\n"
|
data/lib/net/nex.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../uri/nex'
|
4
|
+
require_relative 'text/generic'
|
5
|
+
|
6
|
+
module Net
|
7
|
+
# == A Nex client API for Ruby.
|
8
|
+
#
|
9
|
+
# Net::Nex provides a library which can be used to build Nex
|
10
|
+
# user-agents.
|
11
|
+
#
|
12
|
+
# Net::Nex is designed to work closely with URI.
|
13
|
+
#
|
14
|
+
# == Simple Examples
|
15
|
+
#
|
16
|
+
# All examples assume you have loaded Net::Nex with:
|
17
|
+
#
|
18
|
+
# require 'net/nex'
|
19
|
+
#
|
20
|
+
# This will also require 'uri' so you don't need to require it
|
21
|
+
# separately.
|
22
|
+
#
|
23
|
+
# The Net::Nex methods in the following section do not persist
|
24
|
+
# connections.
|
25
|
+
#
|
26
|
+
# === GET by URI
|
27
|
+
#
|
28
|
+
# uri = URI('nex://nightfall.city/nex/info/')
|
29
|
+
# Net::Nex.get(uri) # => String
|
30
|
+
#
|
31
|
+
class Nex < Text::Generic
|
32
|
+
def self.get(string_or_uri)
|
33
|
+
uri = build_uri string_or_uri, URI::Nex
|
34
|
+
request uri, "#{uri.path}\r\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Net
|
6
|
+
module Text
|
7
|
+
# Generic interface to be used by specific network protocol implementation.
|
8
|
+
class Generic
|
9
|
+
def self.request(uri, query)
|
10
|
+
sock = TCPSocket.new(uri.host, uri.port)
|
11
|
+
sock.puts query
|
12
|
+
sock.read
|
13
|
+
ensure
|
14
|
+
# Stop remaining connection, even if they should be already cut
|
15
|
+
# by the server
|
16
|
+
sock&.close
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build_uri(string_or_uri, uri_class)
|
20
|
+
string_or_uri = URI(string_or_uri) if string_or_uri.is_a?(String)
|
21
|
+
return string_or_uri if string_or_uri.is_a?(uri_class)
|
22
|
+
raise ArgumentError, "uri is not a String, nor an #{uri_class.name}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Text
|
5
|
+
# Contains helper methods to correctly display texts with long lines.
|
6
|
+
#
|
7
|
+
# This module expect given text to be Gemtext inspired (i.e. links
|
8
|
+
# prefixed with => and ``` delimitting code blocks).
|
9
|
+
module Reflow
|
10
|
+
def self.reflow_line_prefix(line)
|
11
|
+
m = line.match(/\A([*#>]+ )/)
|
12
|
+
return '' unless m
|
13
|
+
# Each quote line should begin with the quote mark
|
14
|
+
return m[1] if m[1].start_with?('>')
|
15
|
+
' ' * m[1].length
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.reflow_text_line(line, mono_block_open, length)
|
19
|
+
line.strip!
|
20
|
+
if mono_block_open || line.start_with?('=>') || line.length < length
|
21
|
+
return [line]
|
22
|
+
end
|
23
|
+
output = []
|
24
|
+
prefix = reflow_line_prefix(line)
|
25
|
+
limit_chars = ['-', '', ' '].freeze
|
26
|
+
while line.length > length
|
27
|
+
cut_line = line[0...length]
|
28
|
+
cut_index = limit_chars.map { cut_line.rindex(_1) || -1 }.max
|
29
|
+
break if cut_index.zero? # Better do nothing for now
|
30
|
+
|
31
|
+
output << line[0...cut_index]
|
32
|
+
line = prefix + line[cut_index + 1..]
|
33
|
+
end
|
34
|
+
output << line
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.format_body(body, length)
|
38
|
+
unless length.is_a? Integer
|
39
|
+
raise ArgumentError, "Length must be Integer, #{length.class} given"
|
40
|
+
end
|
41
|
+
return body if length.zero?
|
42
|
+
|
43
|
+
new_body = []
|
44
|
+
mono_block_open = false
|
45
|
+
buf = StringIO.new(body)
|
46
|
+
while (line = buf.gets)
|
47
|
+
if line.start_with?('```')
|
48
|
+
mono_block_open = !mono_block_open
|
49
|
+
# Don't include code block toggle lines
|
50
|
+
next
|
51
|
+
end
|
52
|
+
new_body += reflow_text_line(line, mono_block_open, length)
|
53
|
+
end
|
54
|
+
new_body.join("\n")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/uri/nex.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module URI # :nodoc:
|
6
|
+
#
|
7
|
+
# The syntax of Nex URIs is defined in the Nex specification,
|
8
|
+
# section 1.2.
|
9
|
+
#
|
10
|
+
# @see https://gemini.circumlunar.space/docs/specification.html
|
11
|
+
#
|
12
|
+
class Nex < HTTP
|
13
|
+
# A Default port of 1900 for URI::Nex.
|
14
|
+
DEFAULT_PORT = 1900
|
15
|
+
|
16
|
+
# An Array of the available components for URI::Nex.
|
17
|
+
COMPONENT = [:scheme, :host, :port, :path].freeze
|
18
|
+
end
|
19
|
+
|
20
|
+
if respond_to? :register_scheme
|
21
|
+
# Introduced somewhere in ruby 3.0.x
|
22
|
+
register_scheme 'NEX', Nex
|
23
|
+
else
|
24
|
+
@@schemes['NEX'] = Nex
|
25
|
+
end
|
26
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-net-text
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
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-
|
11
|
+
date: 2023-10-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: etienne@depar.is
|
@@ -20,16 +20,20 @@ files:
|
|
20
20
|
- README.md
|
21
21
|
- lib/net/finger.rb
|
22
22
|
- lib/net/gemini.rb
|
23
|
-
- lib/net/gemini/
|
24
|
-
- lib/net/gemini/
|
23
|
+
- lib/net/gemini/client.rb
|
24
|
+
- lib/net/gemini/client/ssl.rb
|
25
|
+
- lib/net/gemini/error.rb
|
25
26
|
- lib/net/gemini/request.rb
|
26
27
|
- lib/net/gemini/response.rb
|
27
|
-
- lib/net/gemini/
|
28
|
-
- lib/net/generic.rb
|
28
|
+
- lib/net/gemini/response/parser.rb
|
29
29
|
- lib/net/gopher.rb
|
30
|
+
- lib/net/nex.rb
|
31
|
+
- lib/net/text/generic.rb
|
32
|
+
- lib/net/text/reflow.rb
|
30
33
|
- lib/uri/finger.rb
|
31
34
|
- lib/uri/gemini.rb
|
32
35
|
- lib/uri/gopher.rb
|
36
|
+
- lib/uri/nex.rb
|
33
37
|
homepage: https://git.umaneti.net/ruby-net-text/
|
34
38
|
licenses:
|
35
39
|
- MIT
|
@@ -38,7 +42,13 @@ metadata:
|
|
38
42
|
source_code_uri: https://git.umaneti.net/ruby-net-text/
|
39
43
|
documentation_uri: https://www.rubydoc.info/gems/ruby-net-text
|
40
44
|
funding_uri: https://liberapay.com/milouse
|
41
|
-
post_install_message:
|
45
|
+
post_install_message: |+
|
46
|
+
The version 0.0.9 introduces some breaking changes in Gemini support.
|
47
|
+
If you were using its internal API instead of just using the documented
|
48
|
+
methods, please refer to the README to know more about the changes.
|
49
|
+
|
50
|
+
https://git.umaneti.net/ruby-net-text/about/
|
51
|
+
|
42
52
|
rdoc_options: []
|
43
53
|
require_paths:
|
44
54
|
- lib
|
@@ -53,8 +63,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
63
|
- !ruby/object:Gem::Version
|
54
64
|
version: '0'
|
55
65
|
requirements: []
|
56
|
-
rubygems_version: 3.4.
|
66
|
+
rubygems_version: 3.4.20
|
57
67
|
signing_key:
|
58
68
|
specification_version: 4
|
59
|
-
summary: Gemini, Gopher
|
69
|
+
summary: Finger, Gemini, Gopher and Nex support for Net::* and URI::*
|
60
70
|
test_files: []
|
71
|
+
...
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Gemini
|
4
|
-
# Contains specific method to parse text/gemini documents.
|
5
|
-
module GmiParser
|
6
|
-
private
|
7
|
-
|
8
|
-
def parse_meta
|
9
|
-
header = { status: @status, meta: @meta, mimetype: nil }
|
10
|
-
return header unless body_permitted?
|
11
|
-
mime = { lang: nil, charset: 'utf-8', format: nil }
|
12
|
-
raw_meta = meta.split(';').map(&:strip)
|
13
|
-
header[:mimetype] = raw_meta.shift
|
14
|
-
return header unless raw_meta.any?
|
15
|
-
raw_meta.map { |m| m.split('=') }.each do |opt|
|
16
|
-
key = opt[0].downcase.to_sym
|
17
|
-
next unless mime.has_key? key
|
18
|
-
mime[key] = opt[1].downcase
|
19
|
-
end
|
20
|
-
header.merge(mime)
|
21
|
-
end
|
22
|
-
|
23
|
-
def parse_preformatted_block(line, buf)
|
24
|
-
cur_block = { meta: line[3..].chomp, content: '' }
|
25
|
-
while (line = buf.gets)
|
26
|
-
if line.start_with?('```')
|
27
|
-
@preformatted_blocks << cur_block
|
28
|
-
break
|
29
|
-
end
|
30
|
-
cur_block[:content] += line
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def parse_link(line)
|
35
|
-
m = line.strip.match(/\A=>\s*([^\s]+)(?:\s*(.+))?\z/)
|
36
|
-
return if m.nil?
|
37
|
-
begin
|
38
|
-
uri = URI(m[1])
|
39
|
-
rescue URI::InvalidURIError
|
40
|
-
return
|
41
|
-
end
|
42
|
-
uri = @uri.merge(uri) if @uri && uri.is_a?(URI::Generic)
|
43
|
-
@links << { uri: uri, label: m[2]&.chomp }
|
44
|
-
end
|
45
|
-
|
46
|
-
def parse_body
|
47
|
-
buf = StringIO.new(@body)
|
48
|
-
while (line = buf.gets)
|
49
|
-
if line.start_with?('```')
|
50
|
-
parse_preformatted_block(line, buf)
|
51
|
-
elsif line.start_with?('=>')
|
52
|
-
parse_link(line)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Gemini
|
4
|
-
# Contains specific method to correctly display Gemini texts
|
5
|
-
module ReflowText
|
6
|
-
private
|
7
|
-
|
8
|
-
def reflow_line_cut_index(line)
|
9
|
-
possible_cut = [
|
10
|
-
line.rindex(' ') || 0,
|
11
|
-
line.rindex('') || 0,
|
12
|
-
line.rindex('-') || 0
|
13
|
-
].sort
|
14
|
-
possible_cut.reverse!
|
15
|
-
possible_cut[0]
|
16
|
-
end
|
17
|
-
|
18
|
-
def reflow_line_prefix(line)
|
19
|
-
m = line.match(/\A([*#>]+ )/)
|
20
|
-
return '' unless m
|
21
|
-
# Each quote line should begin with the quote mark
|
22
|
-
return m[1] if m[1].start_with?('>')
|
23
|
-
' ' * m[1].length
|
24
|
-
end
|
25
|
-
|
26
|
-
def reflow_text_line(line, mono_block_open, length)
|
27
|
-
line.strip!
|
28
|
-
if mono_block_open || line.start_with?('=>') || line.length < length
|
29
|
-
return [line]
|
30
|
-
end
|
31
|
-
output = []
|
32
|
-
prefix = reflow_line_prefix(line)
|
33
|
-
while line.length > length
|
34
|
-
cut_line = line[0...length]
|
35
|
-
cut_index = reflow_line_cut_index(cut_line)
|
36
|
-
break if cut_index.zero? # Better do nothing for now
|
37
|
-
output << line[0...cut_index]
|
38
|
-
line = prefix + line[cut_index + 1..]
|
39
|
-
end
|
40
|
-
output << line
|
41
|
-
end
|
42
|
-
|
43
|
-
def reformat_body(length)
|
44
|
-
unless length.is_a? Integer
|
45
|
-
raise ArgumentError, "Length must be Integer, #{length} given"
|
46
|
-
end
|
47
|
-
return @body if length.zero?
|
48
|
-
new_body = []
|
49
|
-
mono_block_open = false
|
50
|
-
buf = StringIO.new(@body)
|
51
|
-
while (line = buf.gets)
|
52
|
-
if line.start_with?('```')
|
53
|
-
mono_block_open = !mono_block_open
|
54
|
-
# Don't include code block toggle lines
|
55
|
-
next
|
56
|
-
end
|
57
|
-
new_body += reflow_text_line(line, mono_block_open, length)
|
58
|
-
end
|
59
|
-
new_body.join("\n")
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
data/lib/net/gemini/ssl.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Gemini
|
4
|
-
# Contains specific method to handle SSL connection
|
5
|
-
module SSL
|
6
|
-
private
|
7
|
-
|
8
|
-
def ssl_check_existing(new_cert, cert_file)
|
9
|
-
raw = File.read cert_file
|
10
|
-
saved_one = OpenSSL::X509::Certificate.new raw
|
11
|
-
return true if saved_one == new_cert
|
12
|
-
# TODO: offer some kind of recuperation
|
13
|
-
warn "#{cert_file} does not match the current host cert!"
|
14
|
-
false
|
15
|
-
end
|
16
|
-
|
17
|
-
def ssl_verify_cb(cert)
|
18
|
-
return false unless OpenSSL::SSL.verify_certificate_identity(cert, @host)
|
19
|
-
cert_file = File.expand_path("#{@certs_path}/#{@host}.pem")
|
20
|
-
return ssl_check_existing(cert, cert_file) if File.exist?(cert_file)
|
21
|
-
FileUtils.mkdir_p(File.expand_path(@certs_path))
|
22
|
-
File.open(cert_file, 'wb') { |f| f.print cert.to_pem }
|
23
|
-
true
|
24
|
-
end
|
25
|
-
|
26
|
-
def ssl_context
|
27
|
-
ssl_context = OpenSSL::SSL::SSLContext.new
|
28
|
-
ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
29
|
-
ssl_context.min_version = OpenSSL::SSL::TLS1_2_VERSION
|
30
|
-
ssl_context.verify_hostname = true
|
31
|
-
ssl_context.ca_file = '/etc/ssl/certs/ca-certificates.crt'
|
32
|
-
ssl_context.verify_callback = lambda do |preverify_ok, store_context|
|
33
|
-
return true if preverify_ok
|
34
|
-
ssl_verify_cb store_context.current_cert
|
35
|
-
end
|
36
|
-
ssl_context
|
37
|
-
end
|
38
|
-
|
39
|
-
def init_sockets
|
40
|
-
socket = TCPSocket.new(@host, @port)
|
41
|
-
@ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
42
|
-
# Close underlying TCP socket with SSL socket
|
43
|
-
@ssl_socket.sync_close = true
|
44
|
-
@ssl_socket.hostname = @host # SNI
|
45
|
-
@ssl_socket.connect
|
46
|
-
end
|
47
|
-
|
48
|
-
# Closes the SSL and TCP connections.
|
49
|
-
def finish
|
50
|
-
@ssl_socket.close
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
data/lib/net/generic.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'socket'
|
4
|
-
|
5
|
-
# Generic interface to be used by specific network protocol implementation.
|
6
|
-
module TextGeneric
|
7
|
-
private
|
8
|
-
|
9
|
-
def build_uri(string_or_uri, uri_class)
|
10
|
-
string_or_uri = URI(string_or_uri) if string_or_uri.is_a?(String)
|
11
|
-
return string_or_uri if string_or_uri.is_a?(uri_class)
|
12
|
-
raise ArgumentError, "uri is not a String, nor an #{uri_class.name}"
|
13
|
-
end
|
14
|
-
|
15
|
-
def request(uri, query)
|
16
|
-
sock = TCPSocket.new(uri.host, uri.port)
|
17
|
-
sock.puts query
|
18
|
-
sock.read
|
19
|
-
ensure
|
20
|
-
# Stop remaining connection, even if they should be already cut
|
21
|
-
# by the server
|
22
|
-
sock&.close
|
23
|
-
end
|
24
|
-
end
|