ruby-net-text 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +35 -7
- data/lib/net/finger.rb +2 -4
- data/lib/net/gemini/client/ssl.rb +63 -0
- data/lib/net/gemini/client.rb +98 -0
- data/lib/net/gemini/error.rb +9 -0
- data/lib/net/gemini/request.rb +40 -36
- data/lib/net/gemini/response/parser.rb +69 -0
- data/lib/net/gemini/response.rb +99 -84
- data/lib/net/gemini.rb +8 -91
- data/lib/net/gopher.rb +2 -4
- data/lib/net/nex.rb +37 -0
- data/lib/net/text/generic.rb +27 -0
- data/lib/net/text/reflow.rb +54 -0
- data/lib/uri/finger.rb +3 -1
- data/lib/uri/gemini.rb +2 -3
- data/lib/uri/gopher.rb +4 -2
- 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: 2610c775fb8178c5c035a7ffbb9176ba679a15fcd061fed7e192b3d83b609727
|
4
|
+
data.tar.gz: 386863cda1e5682d05bc75a52da55ae4d972393db7efa38fed7d259b44f9e717
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c7663972f81f9f93e02bf76bccfaa1b158b4034259617e94856d27fb63aa5368def164135cea3318c355e3e137ae0c6b852b96194c52db9180aecd318a8d3e1
|
7
|
+
data.tar.gz: 61bff20043c1866c3075312a87f9a68dc7d7aa7fd869d8962672dc2ce6eeed9ad065d6ca0bc8330bc94a41fa041933a7bf0da20c0871826d805a0febf2d8dcb2
|
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
|
[![Support using Liberapay](https://img.shields.io/badge/Liberapay-Support_me-yellow?logo=liberapay)](https://liberapay.com/milouse/donate)
|
4
4
|
[![Support using Flattr](https://img.shields.io/badge/Flattr-Support_me-brightgreen?logo=flattr)](https://flattr.com/@milouse)
|
5
5
|
[![Support using Paypal](https://img.shields.io/badge/Paypal-Support_me-00457C?logo=paypal&labelColor=lightgray)](https://paypal.me/milouse)
|
6
6
|
|
7
7
|
[![Gem](https://img.shields.io/gem/v/ruby-net-text)](https://rubygems.org/gems/ruby-net-text)
|
8
|
-
[![Documentation](https://img.shields.io/badge/Documentation-ruby--net--text-CC342D?logo=rubygems)](https://www.rubydoc.info/gems/ruby-net-text/
|
8
|
+
[![Documentation](https://img.shields.io/badge/Documentation-ruby--net--text-CC342D?logo=rubygems)](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,63 @@
|
|
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
|
+
|
27
|
+
FileUtils.mkdir_p(File.expand_path(@certs_path))
|
28
|
+
File.open(cert_file, 'wb') { |f| f.print cert.to_pem }
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def ssl_context
|
33
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
34
|
+
ssl_context.set_params(
|
35
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
36
|
+
min_version: OpenSSL::SSL::TLS1_2_VERSION,
|
37
|
+
ca_file: '/etc/ssl/certs/ca-certificates.crt',
|
38
|
+
verify_hostname: true
|
39
|
+
)
|
40
|
+
ssl_context.verify_callback = lambda do |preverify_ok, store_context|
|
41
|
+
return true if preverify_ok
|
42
|
+
|
43
|
+
ssl_verify_cb store_context.current_cert
|
44
|
+
end
|
45
|
+
ssl_context
|
46
|
+
end
|
47
|
+
|
48
|
+
def init_sockets
|
49
|
+
socket = TCPSocket.new(@host, @port)
|
50
|
+
@ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
|
51
|
+
# Close underlying TCP socket with SSL socket
|
52
|
+
@ssl_socket.sync_close = true
|
53
|
+
@ssl_socket.hostname = @host # SNI
|
54
|
+
@ssl_socket.connect
|
55
|
+
end
|
56
|
+
|
57
|
+
# Closes the SSL and TCP connections.
|
58
|
+
def finish
|
59
|
+
@ssl_socket.close
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'error'
|
4
|
+
require_relative 'request'
|
5
|
+
require_relative 'response'
|
6
|
+
require_relative '../text/generic'
|
7
|
+
|
8
|
+
module Net
|
9
|
+
module Gemini # rubocop:disable Style/Documentation
|
10
|
+
# An example client to fetch resources hosted on Gemini network.
|
11
|
+
class Client
|
12
|
+
attr_writer :certs_path
|
13
|
+
|
14
|
+
def initialize(host, port)
|
15
|
+
@host = host
|
16
|
+
@port = port
|
17
|
+
@certs_path = '~/.cache/gemini/certs'
|
18
|
+
end
|
19
|
+
|
20
|
+
# This method can raise an OpenSSL::SSL::SSLError
|
21
|
+
def request!(uri)
|
22
|
+
init_sockets
|
23
|
+
req = Request.new uri
|
24
|
+
req.write @ssl_socket
|
25
|
+
res = Response.read_new(@ssl_socket)
|
26
|
+
res.uri = uri
|
27
|
+
res.reading_body(@ssl_socket)
|
28
|
+
end
|
29
|
+
|
30
|
+
def request(uri)
|
31
|
+
request! uri
|
32
|
+
rescue OpenSSL::SSL::SSLError => e
|
33
|
+
msg = format(
|
34
|
+
'SSLError: %<cause>s',
|
35
|
+
cause: e.message.sub(/.*state=error: (.+)\Z/, '\1')
|
36
|
+
)
|
37
|
+
Response.new('59', msg)
|
38
|
+
ensure
|
39
|
+
# Stop remaining connection, even if they should be already cut
|
40
|
+
# by the server
|
41
|
+
finish
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch(uri, limit = 5)
|
45
|
+
raise Error, 'Too many Gemini redirects' if limit.zero?
|
46
|
+
|
47
|
+
r = request(uri)
|
48
|
+
return r unless r.status[0] == '3'
|
49
|
+
|
50
|
+
begin
|
51
|
+
uri = handle_redirect(r)
|
52
|
+
rescue ArgumentError, URI::InvalidURIError
|
53
|
+
return r
|
54
|
+
end
|
55
|
+
warn "Redirect to #{uri}" if $VERBOSE
|
56
|
+
fetch(uri, limit - 1)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def handle_redirect(response)
|
62
|
+
uri = response.uri
|
63
|
+
old_url = uri.to_s
|
64
|
+
new_uri = URI(response.meta)
|
65
|
+
uri.merge!(new_uri)
|
66
|
+
raise Error, "Redirect loop on #{uri}" if uri.to_s == old_url
|
67
|
+
|
68
|
+
@host = uri.host
|
69
|
+
@port = uri.port
|
70
|
+
uri
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.start(host_or_uri, port = nil, &block)
|
75
|
+
if host_or_uri.is_a? URI::Gemini
|
76
|
+
host = host_or_uri.host
|
77
|
+
port = host_or_uri.port
|
78
|
+
else
|
79
|
+
host = host_or_uri
|
80
|
+
end
|
81
|
+
gem = Client.new(host, port)
|
82
|
+
return gem unless block
|
83
|
+
|
84
|
+
yield gem
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.get_response(uri)
|
88
|
+
start(uri.host, uri.port) { |gem| gem.fetch(uri) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.get(string_or_uri)
|
92
|
+
uri = Net::Text::Generic.build_uri string_or_uri, URI::Gemini
|
93
|
+
get_response(uri).body
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
require_relative 'client/ssl'
|
data/lib/net/gemini/request.rb
CHANGED
@@ -2,48 +2,52 @@
|
|
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
|
-
raise
|
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://geminiprotocol.net/docs/protocol-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
|
+
raise BadRequest, "Request too long: #{url.dump}" if url.length > 1024
|
25
|
+
|
26
|
+
@uri = URI(url)
|
27
|
+
return if uri.is_a? URI::Gemini
|
28
|
+
|
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
|
+
|
49
|
+
new(m[1])
|
50
|
+
end
|
47
51
|
end
|
48
52
|
end
|
49
53
|
end
|
@@ -0,0 +1,69 @@
|
|
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 received_mime(raw_meta)
|
11
|
+
meta_data = raw_meta.map { |m| m.split('=') }
|
12
|
+
mime = { lang: nil, charset: 'utf-8', format: nil }
|
13
|
+
new_mime = meta_data.filter_map do |opt|
|
14
|
+
key = opt[0].downcase.to_sym
|
15
|
+
next unless mime.has_key? key
|
16
|
+
|
17
|
+
[key, opt[1].downcase]
|
18
|
+
end
|
19
|
+
mime.merge new_mime.to_h
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_meta
|
23
|
+
header = { status: @status, meta: @meta, mimetype: nil }
|
24
|
+
return header unless body_permitted?
|
25
|
+
|
26
|
+
raw_meta = meta.split(';').map(&:strip)
|
27
|
+
header[:mimetype] = raw_meta.shift
|
28
|
+
return header unless raw_meta.any?
|
29
|
+
|
30
|
+
header.merge received_mime(raw_meta)
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_preformatted_block(line, buf)
|
34
|
+
cur_block = { meta: line[3..].chomp, content: '' }
|
35
|
+
while (line = buf.gets)
|
36
|
+
if line.start_with?('```')
|
37
|
+
@preformatted_blocks << cur_block
|
38
|
+
break
|
39
|
+
end
|
40
|
+
cur_block[:content] += line
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_link(line)
|
45
|
+
m = line.strip.match(/\A=>\s*([^\s]+)(?:\s*(.+))?\z/)
|
46
|
+
return if m.nil?
|
47
|
+
|
48
|
+
begin
|
49
|
+
uri = URI(m[1])
|
50
|
+
rescue URI::InvalidURIError
|
51
|
+
return
|
52
|
+
end
|
53
|
+
uri = @uri.merge(uri) if @uri && uri.is_a?(URI::Generic)
|
54
|
+
@links << { uri: uri, label: m[2]&.chomp }
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_body
|
58
|
+
buf = StringIO.new(@body)
|
59
|
+
while (line = buf.gets)
|
60
|
+
if line.start_with?('```')
|
61
|
+
parse_preformatted_block(line, buf)
|
62
|
+
elsif line.start_with?('=>')
|
63
|
+
parse_link(line)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/net/gemini/response.rb
CHANGED
@@ -3,103 +3,118 @@
|
|
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://geminiprotocol.net/docs/protocol-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
|
+
|
56
|
+
def reading_body(sock)
|
57
|
+
return self unless body_permitted?
|
55
58
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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'
|
63
|
+
|
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
|
+
|
76
|
+
unless reflow_at.is_a? Integer
|
77
|
+
raise(
|
78
|
+
ArgumentError, "reflow_at must be Integer, #{reflow_at.class} given"
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
return @body if reflow_at <= 0 || @header[:format] == 'fixed'
|
73
83
|
|
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)
|
84
|
+
Net::Text::Reflow.format_body(@body, reflow_at)
|
84
85
|
end
|
85
|
-
end
|
86
86
|
|
87
|
-
|
87
|
+
class << self
|
88
|
+
def read_new(sock)
|
89
|
+
# Read up to 1029 bytes:
|
90
|
+
# - 3 bytes for code and space separator
|
91
|
+
# - 1024 bytes max for the message
|
92
|
+
# - 2 bytes for <CR><LF>
|
93
|
+
str = sock.gets($INPUT_RECORD_SEPARATOR, 1029)
|
94
|
+
m = /\A([1-6]\d) (.*)\r\n\z/.match(str)
|
95
|
+
raise BadResponse, "wrong status line: #{str.dump}" if m.nil?
|
96
|
+
|
97
|
+
new(*m.captures)
|
98
|
+
end
|
99
|
+
end
|
88
100
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
101
|
+
private
|
102
|
+
|
103
|
+
def encode_body(body)
|
104
|
+
return body unless @header[:mimetype].start_with?('text/')
|
105
|
+
|
106
|
+
if @header[:charset] && @header[:charset] != 'utf-8'
|
107
|
+
# If body use another charset than utf-8, we need first to
|
108
|
+
# declare the raw byte string as using this chasret
|
109
|
+
body.force_encoding(@header[:charset])
|
110
|
+
# Then we can safely try to convert it to utf-8
|
111
|
+
return body.encode('utf-8')
|
112
|
+
end
|
113
|
+
# Just declare that the body uses utf-8
|
114
|
+
body.force_encoding('utf-8')
|
97
115
|
end
|
98
|
-
# Just declare that the body uses utf-8
|
99
|
-
body.force_encoding('utf-8')
|
100
116
|
end
|
101
|
-
|
102
|
-
include ::Gemini::GmiParser
|
103
|
-
include ::Gemini::ReflowText
|
104
117
|
end
|
105
118
|
end
|
119
|
+
|
120
|
+
require_relative 'response/parser'
|
data/lib/net/gemini.rb
CHANGED
@@ -6,19 +6,12 @@ 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
|
20
13
|
# user-agents.
|
21
|
-
# @see
|
14
|
+
# @see gemini://geminiprotocol.net/docs/specification.gmi
|
22
15
|
#
|
23
16
|
# Net::Gemini is designed to work closely with URI.
|
24
17
|
#
|
@@ -36,8 +29,7 @@ module Net
|
|
36
29
|
#
|
37
30
|
# === GET by URI
|
38
31
|
#
|
39
|
-
#
|
40
|
-
# Net::Gemini.get(uri) # => String
|
32
|
+
# Net::Gemini.get('gemini://gemini.circumlunar.space/') # => String
|
41
33
|
#
|
42
34
|
# === GET with Dynamic Parameters
|
43
35
|
#
|
@@ -66,12 +58,12 @@ module Net
|
|
66
58
|
#
|
67
59
|
# # Body
|
68
60
|
# puts res.body if res.body_permitted?
|
69
|
-
# puts res.body(
|
61
|
+
# puts res.body(reflow_at: 85)
|
70
62
|
#
|
71
63
|
# === Following Redirection
|
72
64
|
#
|
73
|
-
# The {#fetch} method, contrary to the {#request} one will try
|
74
|
-
# automatically resolves redirection, leading you to the final
|
65
|
+
# The {Client#fetch} method, contrary to the {Client#request} one will try
|
66
|
+
# to automatically resolves redirection, leading you to the final
|
75
67
|
# destination.
|
76
68
|
#
|
77
69
|
# u = URI('gemini://exemple.com/redirect')
|
@@ -88,82 +80,7 @@ module Net
|
|
88
80
|
# puts "#{res.status} - #{res.meta}" # => '20 - text/gemini;'
|
89
81
|
# puts res.uri.to_s # => 'gemini://exemple.com/final/dest'
|
90
82
|
#
|
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
|
83
|
+
module Gemini; end
|
169
84
|
end
|
85
|
+
|
86
|
+
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,27 @@
|
|
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
|
+
|
23
|
+
raise ArgumentError, "uri is not a String, nor an #{uri_class.name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,54 @@
|
|
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
|
+
|
16
|
+
' ' * m[1].length
|
17
|
+
end
|
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
|
+
|
25
|
+
output = []
|
26
|
+
prefix = reflow_line_prefix(line)
|
27
|
+
limit_chars = ['-', '', ' '].freeze
|
28
|
+
while line.length > length
|
29
|
+
cut_line = line[0...length]
|
30
|
+
cut_index = limit_chars.map { cut_line.rindex(_1) || -1 }.max
|
31
|
+
break if cut_index.zero? # Better do nothing for now
|
32
|
+
|
33
|
+
output << line[0...cut_index]
|
34
|
+
line = prefix + line[cut_index + 1..]
|
35
|
+
end
|
36
|
+
output << line
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.format_body(body, length)
|
40
|
+
new_body = []
|
41
|
+
mono_block_open = false
|
42
|
+
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)
|
49
|
+
end
|
50
|
+
new_body.join("\n")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/uri/finger.rb
CHANGED
@@ -14,13 +14,15 @@ module URI # :nodoc:
|
|
14
14
|
DEFAULT_PORT = 79
|
15
15
|
|
16
16
|
# An Array of the available components for URI::Finger.
|
17
|
-
COMPONENT = [
|
17
|
+
COMPONENT = %i[scheme userinfo host port path].freeze
|
18
18
|
|
19
19
|
def name
|
20
20
|
# Utilitary method to extract a name to query, either from the userinfo
|
21
21
|
# part or from the path.
|
22
22
|
return @user if @user
|
23
|
+
|
23
24
|
return '' unless @path
|
25
|
+
|
24
26
|
# Remove leading /
|
25
27
|
@path[1..] || ''
|
26
28
|
end
|
data/lib/uri/gemini.rb
CHANGED
@@ -7,15 +7,14 @@ module URI # :nodoc:
|
|
7
7
|
# The syntax of Gemini URIs is defined in the Gemini specification,
|
8
8
|
# section 1.2.
|
9
9
|
#
|
10
|
-
# @see https://
|
10
|
+
# @see https://geminiprotocol.net/docs/protocol-specification.html
|
11
11
|
#
|
12
12
|
class Gemini < HTTP
|
13
13
|
# A Default port of 1965 for URI::Gemini.
|
14
14
|
DEFAULT_PORT = 1965
|
15
15
|
|
16
16
|
# An Array of the available components for URI::Gemini.
|
17
|
-
COMPONENT = [
|
18
|
-
:path, :query, :fragment].freeze
|
17
|
+
COMPONENT = %i[scheme host port path query fragment].freeze
|
19
18
|
end
|
20
19
|
|
21
20
|
if respond_to? :register_scheme
|
data/lib/uri/gopher.rb
CHANGED
@@ -14,22 +14,24 @@ module URI # :nodoc:
|
|
14
14
|
DEFAULT_PORT = 70
|
15
15
|
|
16
16
|
# An Array of the available components for URI::Gopher.
|
17
|
-
COMPONENT = [
|
17
|
+
COMPONENT = %i[scheme host port path].freeze
|
18
18
|
|
19
19
|
def selector
|
20
20
|
return @selector if defined? @selector
|
21
|
+
|
21
22
|
@selector = extract_gopher_path_elements[1]
|
22
23
|
end
|
23
24
|
|
24
25
|
def item_type
|
25
26
|
return @item_type if defined? @item_type
|
27
|
+
|
26
28
|
@item_type = extract_gopher_path_elements[0]
|
27
29
|
end
|
28
30
|
|
29
31
|
private
|
30
32
|
|
31
33
|
def extract_gopher_path_elements
|
32
|
-
m =
|
34
|
+
m = %r{\A/([0-9+dfghisA-LT])(/.*)?\Z}.match(@path)
|
33
35
|
m.captures
|
34
36
|
end
|
35
37
|
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 nex://nightfall.city/nex/info/specification.txt
|
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 = %i[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.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Étienne Deparis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-07-13 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.
|
66
|
+
rubygems_version: 3.5.15
|
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
|