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