ruby-net-text 0.0.3 → 0.0.7
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 +39 -0
- data/lib/net/finger.rb +39 -0
- data/lib/net/gemini/gmi_parser.rb +57 -0
- data/lib/net/gemini/reflow_text.rb +62 -0
- data/lib/net/gemini/request.rb +1 -1
- data/lib/net/gemini/ssl.rb +53 -0
- data/lib/net/gemini.rb +2 -2
- data/lib/net/generic.rb +24 -0
- data/lib/net/gopher.rb +39 -0
- data/lib/uri/finger.rb +16 -2
- data/lib/uri/gemini.rb +6 -1
- data/lib/uri/gopher.rb +23 -1
- metadata +15 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de26ac5c1f8127c9d08a5430adc0cff34d266847327c6864baa903b5352ac041
|
4
|
+
data.tar.gz: d2725dab71f60b4d12be9ff0d948ae1a3f1bc6de9858499258aeedc11c801ae0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79defc399e2b1e1631e153c5994ef1547d0eb9732f0153a641ba9dbf98d35ea03c176723b7bb7052ac08a61b2069eac37e158d04e213dd4c778ecb0c39af00b4
|
7
|
+
data.tar.gz: 050fecb1c2fda499404207e5cb3d49048ff5fcb9695497f35b257dc40c02e3e71ba648ffe575e044a57e282ac43f62590d147c65404005427ae8bfeeb66848d9
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Gemini, Gopher, and Finger support for Net::* and URI::*
|
2
|
+
|
3
|
+
[](https://liberapay.com/milouse/donate)
|
4
|
+
[](https://flattr.com/@milouse)
|
5
|
+
[](https://paypal.me/milouse)
|
6
|
+
|
7
|
+
[](https://rubygems.org/gems/ruby-net-text)
|
8
|
+
[](https://www.rubydoc.info/gems/ruby-net-text/Net/Gemini)
|
9
|
+
|
10
|
+
This project aims to add connectors to well known internet text protocols
|
11
|
+
through the standard `Net::*` and `URI::*` ruby module namespaces.
|
12
|
+
|
13
|
+
## Documentation
|
14
|
+
|
15
|
+
The code is self-documented and you can browse it on rubydoc.info:
|
16
|
+
|
17
|
+
### Gemini
|
18
|
+
|
19
|
+
- [URI::Gemini](https://www.rubydoc.info/gems/ruby-net-text/URI/Gemini)
|
20
|
+
- [Net::Gemini](https://www.rubydoc.info/gems/ruby-net-text/Net/Gemini)
|
21
|
+
|
22
|
+
### Gopher
|
23
|
+
|
24
|
+
- [URI::Gopher](https://www.rubydoc.info/gems/ruby-net-text/URI/Gopher)
|
25
|
+
- [Net::Gopher](https://www.rubydoc.info/gems/ruby-net-text/Net/Gopher)
|
26
|
+
|
27
|
+
### Finger
|
28
|
+
|
29
|
+
- [URI::Finger](https://www.rubydoc.info/gems/ruby-net-text/URI/Finger)
|
30
|
+
- [Net::Finger](https://www.rubydoc.info/gems/ruby-net-text/Net/Finger)
|
31
|
+
|
32
|
+
## Helpers
|
33
|
+
|
34
|
+
This repository also includes 2 little helpers:
|
35
|
+
|
36
|
+
- `bin/heraut`: a toy client for Gemini, Gopher and Finger. Give it a URI and
|
37
|
+
it will output the remote file.
|
38
|
+
- `bin/test_thread.rb`: a toy performance test script to run against a Gemini
|
39
|
+
server
|
data/lib/net/finger.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../uri/finger'
|
4
|
+
require_relative 'generic'
|
5
|
+
|
6
|
+
module Net
|
7
|
+
# == A Finger client API for Ruby.
|
8
|
+
#
|
9
|
+
# Net::Finger provides a library which can be used to build Finger
|
10
|
+
# user-agents.
|
11
|
+
#
|
12
|
+
# Net::Finger is designed to work closely with URI.
|
13
|
+
#
|
14
|
+
# == Simple Examples
|
15
|
+
#
|
16
|
+
# All examples assume you have loaded Net::Finger with:
|
17
|
+
#
|
18
|
+
# require 'net/finger'
|
19
|
+
#
|
20
|
+
# This will also require 'uri' so you don't need to require it
|
21
|
+
# separately.
|
22
|
+
#
|
23
|
+
# The Net::Finger methods in the following section do not persist
|
24
|
+
# connections.
|
25
|
+
#
|
26
|
+
# === GET by URI
|
27
|
+
#
|
28
|
+
# uri = URI('finger://skyjake.fi/jaakko')
|
29
|
+
# Net::Finger.get(uri) # => String
|
30
|
+
#
|
31
|
+
class Finger
|
32
|
+
extend TextGeneric
|
33
|
+
|
34
|
+
def self.get(string_or_uri)
|
35
|
+
uri = build_uri string_or_uri, URI::Finger
|
36
|
+
request uri, uri.name.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,57 @@
|
|
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
|
@@ -0,0 +1,62 @@
|
|
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/request.rb
CHANGED
@@ -42,7 +42,7 @@ module Net
|
|
42
42
|
# - 2 bytes for <CR><LF>
|
43
43
|
str = sock.gets($INPUT_RECORD_SEPARATOR, 1026)
|
44
44
|
m = /\A(.*)\r\n\z/.match(str)
|
45
|
-
raise GeminiBadRequest, "Malformed request: #{str
|
45
|
+
raise GeminiBadRequest, "Malformed request: #{str&.dump}" if m.nil?
|
46
46
|
new(m[1])
|
47
47
|
end
|
48
48
|
end
|
@@ -0,0 +1,53 @@
|
|
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/gemini.rb
CHANGED
@@ -16,8 +16,8 @@ module Net
|
|
16
16
|
|
17
17
|
# == A Gemini client API for Ruby.
|
18
18
|
#
|
19
|
-
# Net::Gemini provides a
|
20
|
-
#
|
19
|
+
# Net::Gemini provides a library which can be used to build Gemini
|
20
|
+
# user-agents.
|
21
21
|
# @see https://gemini.circumlunar.space/docs/specification.html
|
22
22
|
#
|
23
23
|
# Net::Gemini is designed to work closely with URI.
|
data/lib/net/generic.rb
ADDED
@@ -0,0 +1,24 @@
|
|
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
|
data/lib/net/gopher.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../uri/gopher'
|
4
|
+
require_relative 'generic'
|
5
|
+
|
6
|
+
module Net
|
7
|
+
# == A Gopher client API for Ruby.
|
8
|
+
#
|
9
|
+
# Net::Gopher provides a library which can be used to build Gopher
|
10
|
+
# user-agents.
|
11
|
+
#
|
12
|
+
# Net::Gopher is designed to work closely with URI.
|
13
|
+
#
|
14
|
+
# == Simple Examples
|
15
|
+
#
|
16
|
+
# All examples assume you have loaded Net::Gopher with:
|
17
|
+
#
|
18
|
+
# require 'net/gopher'
|
19
|
+
#
|
20
|
+
# This will also require 'uri' so you don't need to require it
|
21
|
+
# separately.
|
22
|
+
#
|
23
|
+
# The Net::Gopher methods in the following section do not persist
|
24
|
+
# connections.
|
25
|
+
#
|
26
|
+
# === GET by URI
|
27
|
+
#
|
28
|
+
# uri = URI('gopher://thelambdalab.xyz/1/projects/elpher/')
|
29
|
+
# Net::Gopher.get(uri) # => String
|
30
|
+
#
|
31
|
+
class Gopher
|
32
|
+
extend TextGeneric
|
33
|
+
|
34
|
+
def self.get(string_or_uri)
|
35
|
+
uri = build_uri string_or_uri, URI::Gopher
|
36
|
+
request uri, "#{uri.selector}\r\n"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/uri/finger.rb
CHANGED
@@ -14,8 +14,22 @@ module URI # :nodoc:
|
|
14
14
|
DEFAULT_PORT = 79
|
15
15
|
|
16
16
|
# An Array of the available components for URI::Finger.
|
17
|
-
COMPONENT = [:scheme, :userinfo, :host, :port].freeze
|
17
|
+
COMPONENT = [:scheme, :userinfo, :host, :port, :path].freeze
|
18
|
+
|
19
|
+
def name
|
20
|
+
# Utilitary method to extract a name to query, either from the userinfo
|
21
|
+
# part or from the path.
|
22
|
+
return @user if @user
|
23
|
+
return '' unless @path
|
24
|
+
# Remove leading /
|
25
|
+
@path[1..] || ''
|
26
|
+
end
|
18
27
|
end
|
19
28
|
|
20
|
-
|
29
|
+
if respond_to? :register_scheme
|
30
|
+
# Introduced somewhere in ruby 3.0.x
|
31
|
+
register_scheme 'FINGER', Finger
|
32
|
+
else
|
33
|
+
@@schemes['FINGER'] = Finger
|
34
|
+
end
|
21
35
|
end
|
data/lib/uri/gemini.rb
CHANGED
@@ -18,5 +18,10 @@ module URI # :nodoc:
|
|
18
18
|
:path, :query, :fragment].freeze
|
19
19
|
end
|
20
20
|
|
21
|
-
|
21
|
+
if respond_to? :register_scheme
|
22
|
+
# Introduced somewhere in ruby 3.0.x
|
23
|
+
register_scheme 'GEMINI', Gemini
|
24
|
+
else
|
25
|
+
@@schemes['GEMINI'] = Gemini
|
26
|
+
end
|
22
27
|
end
|
data/lib/uri/gopher.rb
CHANGED
@@ -15,7 +15,29 @@ module URI # :nodoc:
|
|
15
15
|
|
16
16
|
# An Array of the available components for URI::Gopher.
|
17
17
|
COMPONENT = [:scheme, :host, :port, :path].freeze
|
18
|
+
|
19
|
+
def selector
|
20
|
+
return @selector if defined? @selector
|
21
|
+
@selector = extract_gopher_path_elements[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def item_type
|
25
|
+
return @item_type if defined? @item_type
|
26
|
+
@item_type = extract_gopher_path_elements[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def extract_gopher_path_elements
|
32
|
+
m = /\A\/([0-9+dfghisA-LT])(\/.*)?\Z/.match(@path)
|
33
|
+
m.captures
|
34
|
+
end
|
18
35
|
end
|
19
36
|
|
20
|
-
|
37
|
+
if respond_to? :register_scheme
|
38
|
+
# Introduced somewhere in ruby 3.0.x
|
39
|
+
register_scheme 'GOPHER', Gopher
|
40
|
+
else
|
41
|
+
@@schemes['GOPHER'] = Gopher
|
42
|
+
end
|
21
43
|
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.7
|
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: 2022-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: etienne@depar.is
|
@@ -17,16 +17,27 @@ extensions: []
|
|
17
17
|
extra_rdoc_files: []
|
18
18
|
files:
|
19
19
|
- LICENSE
|
20
|
+
- README.md
|
21
|
+
- lib/net/finger.rb
|
20
22
|
- lib/net/gemini.rb
|
23
|
+
- lib/net/gemini/gmi_parser.rb
|
24
|
+
- lib/net/gemini/reflow_text.rb
|
21
25
|
- lib/net/gemini/request.rb
|
22
26
|
- lib/net/gemini/response.rb
|
27
|
+
- lib/net/gemini/ssl.rb
|
28
|
+
- lib/net/generic.rb
|
29
|
+
- lib/net/gopher.rb
|
23
30
|
- lib/uri/finger.rb
|
24
31
|
- lib/uri/gemini.rb
|
25
32
|
- lib/uri/gopher.rb
|
26
33
|
homepage: https://git.umaneti.net/ruby-net-text/
|
27
34
|
licenses:
|
28
35
|
- MIT
|
29
|
-
metadata:
|
36
|
+
metadata:
|
37
|
+
rubygems_mfa_required: 'true'
|
38
|
+
source_code_uri: https://git.umaneti.net/ruby-net-text/
|
39
|
+
documentation_uri: https://www.rubydoc.info/gems/ruby-net-text
|
40
|
+
funding_uri: https://liberapay.com/milouse
|
30
41
|
post_install_message:
|
31
42
|
rdoc_options: []
|
32
43
|
require_paths:
|
@@ -42,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
53
|
- !ruby/object:Gem::Version
|
43
54
|
version: '0'
|
44
55
|
requirements: []
|
45
|
-
rubygems_version: 3.
|
56
|
+
rubygems_version: 3.3.7
|
46
57
|
signing_key:
|
47
58
|
specification_version: 4
|
48
59
|
summary: Gemini, Gopher, and Finger support for Net::* and URI::*
|