ruby-net-text 0.0.3 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Support using Liberapay](https://img.shields.io/badge/Liberapay-Support_me-yellow?logo=liberapay)](https://liberapay.com/milouse/donate)
|
4
|
+
[![Support using Flattr](https://img.shields.io/badge/Flattr-Support_me-brightgreen?logo=flattr)](https://flattr.com/@milouse)
|
5
|
+
[![Support using Paypal](https://img.shields.io/badge/Paypal-Support_me-00457C?logo=paypal&labelColor=lightgray)](https://paypal.me/milouse)
|
6
|
+
|
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/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::*
|