opentofu 0.1.0
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/.rubocop.yml +34 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +16 -0
- data/lib/net/tofu/error.rb +27 -0
- data/lib/net/tofu/request.rb +147 -0
- data/lib/net/tofu/response.rb +179 -0
- data/lib/net/tofu/socket.rb +54 -0
- data/lib/net/tofu/version.rb +5 -0
- data/lib/net/tofu.rb +32 -0
- data/lib/uri/gemini.rb +49 -0
- data/sig/opentofu.rbs +4 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1eeac402d86ae7cc5b5f2e9be3327a60f8f3132453854ea9ee5f477462e58fc7
|
4
|
+
data.tar.gz: 54f68d912adb5e14a39ddde842ad4acf5ff3664923815138e5a30fcc8ba87881
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c74b24eada5091a0d37eb1a97b871838744e2169b36974fed374900c0a64e73acafe019751341da153f6f5e59a0b6e48e2c4aa9b1674fa65aa3573a74be44528
|
7
|
+
data.tar.gz: 8c00e948a371088cd8e87698d3884fe55d7aa8b84ac65c5a5d64d6b0a53cbf505fed91010cd4d83e53f820a1b2a6dac46a9ad5947733546404e1d992d0301bf5
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.6
|
3
|
+
|
4
|
+
Metrics/MethodLength:
|
5
|
+
Enabled: true
|
6
|
+
Max: 18
|
7
|
+
Exclude:
|
8
|
+
- "lib/net/tofu/response.rb"
|
9
|
+
|
10
|
+
Style/ClassVars:
|
11
|
+
Enabled: true
|
12
|
+
Exclude:
|
13
|
+
- "lib/net/tofu/request.rb"
|
14
|
+
|
15
|
+
Style/Semicolon:
|
16
|
+
Enabled: true
|
17
|
+
Exclude:
|
18
|
+
- "lib/net/tofu/response.rb"
|
19
|
+
|
20
|
+
Style/SingleLineMethods:
|
21
|
+
Enabled: true
|
22
|
+
Exclude:
|
23
|
+
- "lib/net/tofu/response.rb"
|
24
|
+
|
25
|
+
Style/StringLiterals:
|
26
|
+
Enabled: true
|
27
|
+
EnforcedStyle: double_quotes
|
28
|
+
|
29
|
+
Style/StringLiteralsInInterpolation:
|
30
|
+
Enabled: true
|
31
|
+
EnforcedStyle: double_quotes
|
32
|
+
|
33
|
+
Layout/LineLength:
|
34
|
+
Max: 120
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [0.1.0] - 2023-07027
|
9
|
+
|
10
|
+
First release. Still lots of improvements to be made.
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Custom error classes for the library.
|
15
|
+
- A Request model for handling client requests.
|
16
|
+
- Handles the URI parsing logic.
|
17
|
+
- Holds a Response type.
|
18
|
+
- Does some basic error checking based on the Gemini Protocol specification.
|
19
|
+
- A Response model for handling server responses.
|
20
|
+
- Holds a Socket type.
|
21
|
+
- Does some basic error checking based on the Gemini Protocol specification.
|
22
|
+
- A Socket model for handling raw socket connections, as well as TLS security settings.
|
23
|
+
- A top-level module for making get and get_response requests.
|
24
|
+
- Added a 'trust' parameter to the Net::Tofu.get and Net::Tofu.get_response methods. This will later be used for certificate management, but it isn't in use yet.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Rory Dudley
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# OpenTOFU
|
2
|
+
|
3
|
+
OpenTOFU is a certificate pinning and client library for Geminispace.
|
4
|
+
It is based off of the ['trust on first use'](https://en.wikipedia.org/wiki/Trust_on_first_use) authentication scheme.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
To install the gem standalone:
|
9
|
+
|
10
|
+
```sh
|
11
|
+
gem install opentofu
|
12
|
+
```
|
13
|
+
|
14
|
+
Or to use it as part of a project, place the following line in your Gemfile, then run `bundle install` in your project directory:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem "opentofu", "~> 0.1.0"
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
require "net/tofu"
|
24
|
+
```
|
25
|
+
|
26
|
+
## Credits
|
27
|
+
|
28
|
+
I'd like to thank Étienne Deparis, author of [ruby-net-text](https://git.umaneti.net/ruby-net-text/) for releasing their code under the MIT license so that some of it can be used in this project. In particular, `lib/ui/gemini.rb` is taken straight from that codebase. The license for it is present in the aformentioned file.
|
29
|
+
|
30
|
+
## Development
|
31
|
+
|
32
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
33
|
+
|
34
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
35
|
+
|
36
|
+
## Contributing
|
37
|
+
|
38
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/pinecat/opentofu.
|
39
|
+
|
40
|
+
## License
|
41
|
+
|
42
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "rubocop/rake_task"
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
task default: %i[test rubocop]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Tofu
|
5
|
+
class Response
|
6
|
+
# Raised when a server sends an invalid header.
|
7
|
+
class InvalidHeaderError < StandardError; end
|
8
|
+
|
9
|
+
# Raised when a server sends an invalid status code.
|
10
|
+
class InvalidStatusCodeError < StandardError; end
|
11
|
+
|
12
|
+
# Raised when a server sends an invalid meta field.
|
13
|
+
class InvalidMetaError < StandardError; end
|
14
|
+
|
15
|
+
# Raised when a server sends an invalid redirect link.
|
16
|
+
class InvalidRedirectError < StandardError; end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Request
|
20
|
+
# Raised when a request contains an invalid scheme.
|
21
|
+
class InvalidSchemeError < StandardError; end
|
22
|
+
|
23
|
+
# Raised when a request contains an invalid URI.
|
24
|
+
class InvalidURIError < StandardError; end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Tofu
|
5
|
+
# Stores a client request to a Gemini server.
|
6
|
+
class Request
|
7
|
+
SCHEME = "gemini"
|
8
|
+
|
9
|
+
MAX_URI_BYTESIZE = 1024
|
10
|
+
|
11
|
+
# @return [URI] The full URI object of the request.
|
12
|
+
attr_reader :uri
|
13
|
+
|
14
|
+
# @return [String] The request scheme (i.e. gemini://, http://).
|
15
|
+
attr_reader :scheme
|
16
|
+
|
17
|
+
# @return [String] The hostname of the request.
|
18
|
+
attr_reader :host
|
19
|
+
|
20
|
+
# @return [Integer] The server port to connect on.
|
21
|
+
attr_reader :port
|
22
|
+
|
23
|
+
# @return [String] The requested path on the host.
|
24
|
+
attr_reader :path
|
25
|
+
|
26
|
+
# @return [Array] Additional queries to send to the host.
|
27
|
+
attr_reader :queries
|
28
|
+
|
29
|
+
# @return [String] A fragment to request from the host.
|
30
|
+
attr_reader :fragment
|
31
|
+
|
32
|
+
# @return [Response] The response from the server after calling #{gets}.
|
33
|
+
attr_reader :resp
|
34
|
+
|
35
|
+
# Constructor for the request type.
|
36
|
+
# @param host [String] A host string, optionally with the gemini:// scheme.
|
37
|
+
# @param port [Integer] Optional parameter to specify the server port to connect to.
|
38
|
+
def initialize(host, port: nil)
|
39
|
+
# Keeps track of the current host for links with only paths.
|
40
|
+
@@current_host ||= ""
|
41
|
+
|
42
|
+
@host = host
|
43
|
+
@port = port unless port.nil?
|
44
|
+
determine_host
|
45
|
+
parse_head
|
46
|
+
parse_tail
|
47
|
+
|
48
|
+
puts format
|
49
|
+
|
50
|
+
# Make sure the URI isn't too large
|
51
|
+
if format.bytesize > MAX_URI_BYTESIZE
|
52
|
+
raise InvalidURIError,
|
53
|
+
"The URI is too large, should be #{MAX_URI_BYTESIZE} bytes, instead is #{format.bytesize} bytes"
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create a socket
|
57
|
+
@sock = Socket.new(@host, @port)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Format the URI for sending over a socket to a Gemini server.
|
61
|
+
# @return [String] The URI string appended with a carriage return and linefeed.
|
62
|
+
def format
|
63
|
+
"#{@uri}\r\n"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Connect to the server and try to fetch data.
|
67
|
+
def gets
|
68
|
+
@sock.connect
|
69
|
+
@resp = @sock.gets(self)
|
70
|
+
ensure
|
71
|
+
@sock.close
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Parses the host and the path, and sets the current_host.
|
77
|
+
def determine_host
|
78
|
+
puts "current: #{@@current_host}"
|
79
|
+
@uri = URI(@host)
|
80
|
+
|
81
|
+
unless @uri.host.nil? || @uri.host.empty?
|
82
|
+
@host = @uri.host
|
83
|
+
@@current_host = @host
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
return if @uri.path.nil? || @uri.path.empty?
|
88
|
+
return unless @uri.host.nil? || @uri.host.empty?
|
89
|
+
|
90
|
+
if @uri.path.start_with?("/")
|
91
|
+
raise InvalidURIError, "No host specified" if @@current_host.nil? || @@current_host.empty?
|
92
|
+
|
93
|
+
unless @@current_host.nil? || @@current_host.empty?
|
94
|
+
@uri.host = @@current_host
|
95
|
+
@host = @uri.host
|
96
|
+
@@current_host = @host
|
97
|
+
end
|
98
|
+
|
99
|
+
return
|
100
|
+
end
|
101
|
+
|
102
|
+
paths = @uri.path.split("/")
|
103
|
+
puts paths
|
104
|
+
|
105
|
+
@uri.host = paths[0]
|
106
|
+
@host = @uri.host
|
107
|
+
@@current_host = @host
|
108
|
+
@uri.path = nil if paths.length == 1
|
109
|
+
return unless paths.length > 1
|
110
|
+
|
111
|
+
@uri.path = paths[1..].join("/")
|
112
|
+
@uri.path = "/#{uri.path}"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Parses the scheme, the host, and the port for the request.
|
116
|
+
def parse_head
|
117
|
+
# Check if a scheme was specified, if not, default to gemini://
|
118
|
+
# Also set the port if this happens
|
119
|
+
if @uri.scheme.nil? || @uri.scheme.empty?
|
120
|
+
@uri.scheme = SCHEME
|
121
|
+
@uri.port = URI::Gemini::DEFAULT_PORT
|
122
|
+
end
|
123
|
+
|
124
|
+
# Set member parts
|
125
|
+
@scheme = @uri.scheme
|
126
|
+
@port = @uri.port
|
127
|
+
|
128
|
+
# Check if a scheme is present that isn't gemini://
|
129
|
+
return if @uri.scheme == SCHEME
|
130
|
+
|
131
|
+
raise InvalidSchemeError,
|
132
|
+
"Request uses an invalid scheme (has: #{@uri.scheme}, wants: #{SCHEME}"
|
133
|
+
end
|
134
|
+
|
135
|
+
# Parses the path, the query, and the fragment for the request.
|
136
|
+
def parse_tail
|
137
|
+
# Set path to '/' if one isn't specified
|
138
|
+
@uri.path = "/" if @uri.path.nil? || @uri.path.empty?
|
139
|
+
|
140
|
+
# Set member parts
|
141
|
+
@path = @uri.path
|
142
|
+
@queries = @uri.query.split("&") unless @uri.query.nil? || @uri.query.empty?
|
143
|
+
@fragment = @uri.fragment
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class String # :nodoc:
|
4
|
+
def numerical?
|
5
|
+
to_i.to_s == self
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Net
|
10
|
+
module Tofu
|
11
|
+
# Stores a response from a Gemini server.
|
12
|
+
class Response
|
13
|
+
# Response types
|
14
|
+
INPUT = 1
|
15
|
+
SUCCESS = 2
|
16
|
+
REDIRECT = 3
|
17
|
+
TEMPORARY_FAILURE = 4
|
18
|
+
PERMANENT_FAILURE = 5
|
19
|
+
REQUEST_CERTIFICATE = 6
|
20
|
+
|
21
|
+
# Limits
|
22
|
+
MAX_META_BYTESIZE = 1024
|
23
|
+
|
24
|
+
# @return [String] The full response header from the server.
|
25
|
+
attr_reader :header
|
26
|
+
|
27
|
+
# @return [Integer] The 2-digit, server response status.
|
28
|
+
attr_reader :status
|
29
|
+
|
30
|
+
# @return [Integer] The first digit of the server response status.
|
31
|
+
attr_reader :status_maj
|
32
|
+
|
33
|
+
# @return [Integer] The second digit of the server response status.
|
34
|
+
attr_reader :status_min
|
35
|
+
|
36
|
+
# Dependent on the #{status} ->
|
37
|
+
# => 1x: (INPUT) A prompt line that should be displayed to the user.
|
38
|
+
# => 2x: (SUCCESS) A MIME media type.
|
39
|
+
# => 3x: (REDIRECT) A new URL for the requested resource.
|
40
|
+
# => 4x: (TEMP FAIL) Additional information regarding the temporary failure.
|
41
|
+
# => 5x: (PERM FAIL) Additional information regarding the temporary failure.
|
42
|
+
# => 6x: (RQST CERT) Additional information regarding the client certificate requirements.
|
43
|
+
#
|
44
|
+
# According to the specification for the Gemini Protocol, clients SHOULD close a connection to servers which send
|
45
|
+
# a meta over 1024 bytes. This library complies with this specification, although, it is conceivable that meta
|
46
|
+
# could be an arbitrarily long string.
|
47
|
+
#
|
48
|
+
# @return [String] The UTF-8 encoded message from the response header.
|
49
|
+
attr_reader :meta
|
50
|
+
|
51
|
+
# @return [String] The message body.
|
52
|
+
attr_reader :body
|
53
|
+
|
54
|
+
# Constructor for the response type.
|
55
|
+
# @param data [String] A raw Gemini server response.
|
56
|
+
def initialize(data)
|
57
|
+
@data = data
|
58
|
+
parse
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get a human readable response type.
|
62
|
+
# @return [String] The response type as a human readable string.
|
63
|
+
def type
|
64
|
+
case @status_maj
|
65
|
+
when INPUT then return "Input"
|
66
|
+
when SUCCESS then return "Success"
|
67
|
+
when REDIRECT then return "Redirect"
|
68
|
+
when TEMPORARY_FAILURE then return "Temporary failure"
|
69
|
+
when PERMANENT_FAILURE then return "Permanent failure"
|
70
|
+
when REQUEST_CERTIFICATE then return "Request certificate"
|
71
|
+
end
|
72
|
+
"Unknown"
|
73
|
+
end
|
74
|
+
|
75
|
+
def input?; return true if @status_maj == INPUT; false; end
|
76
|
+
def success?; return true if @status_maj == SUCCESS; false; end
|
77
|
+
def redirect?; return true if @status_maj == REDIRECT; false; end
|
78
|
+
def temporary_failure?; return true if @status_maj == TEMPORARY_FAILURE; false; end
|
79
|
+
def permanent_failure?; return true if @status_maj == PERMANENT_FAILURE; false; end
|
80
|
+
def request_certificate?; return true if @status_maj == REQUEST_CERTIFICATE; false; end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Splits up the responseinto a header and a body.
|
85
|
+
def parse
|
86
|
+
# Extract the header and parse it
|
87
|
+
a = @data.split("\n")
|
88
|
+
@header = a[0].strip
|
89
|
+
parse_header
|
90
|
+
|
91
|
+
# Remove the first element from the array,
|
92
|
+
# then populate body with the rest of the data
|
93
|
+
a.shift
|
94
|
+
@body = a.join("\n") if a.length.positive?
|
95
|
+
end
|
96
|
+
|
97
|
+
# Splits upt the header into a status code and a meta.
|
98
|
+
def parse_header
|
99
|
+
a = @header.split
|
100
|
+
|
101
|
+
# Make sure there are only one or two elements in the header
|
102
|
+
raise InvalidHeaderError, "The server did not send a header" unless a.length.positive?
|
103
|
+
|
104
|
+
# Parse the status code
|
105
|
+
@status = a[0]
|
106
|
+
parse_status
|
107
|
+
|
108
|
+
# Parse the meta
|
109
|
+
@meta = ""
|
110
|
+
@meta = a[1..].join(" ") if a.length >= 2
|
111
|
+
parse_meta
|
112
|
+
end
|
113
|
+
|
114
|
+
# Splits up the status into a major and minor, checks for invalid status codes.
|
115
|
+
def parse_status
|
116
|
+
# Make sure the status is numerical
|
117
|
+
unless @status.numerical?
|
118
|
+
raise InvalidStatusCodeError,
|
119
|
+
"The server sent a non-numerical status code: #{@status}"
|
120
|
+
end
|
121
|
+
|
122
|
+
# Allow status code to only be a single digit, or two digits (as the spec says it should be)
|
123
|
+
if @status.length == 1
|
124
|
+
@status_maj = Integer(@status)
|
125
|
+
@status_min = 0
|
126
|
+
elsif @status.length == 2
|
127
|
+
@status_maj = Integer(@status[0])
|
128
|
+
@status_min = Integer(@status[1])
|
129
|
+
else
|
130
|
+
raise InvalidStatusCodeError, "The server sent a status code that is longer than 2 digits: #{@status}"
|
131
|
+
end
|
132
|
+
|
133
|
+
# Make sure the major status code is between 1 and 6, including 1 and 6
|
134
|
+
unless @status_maj >= 1 && @status_maj <= 6
|
135
|
+
raise InvalidStatusCodeError,
|
136
|
+
"The server sent an invalid, major status code: #{@status_maj}"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Make sure #{status} is an Integer
|
140
|
+
@status = Integer(@status)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Checks the meta size, does extra checking depending on #{status} type.
|
144
|
+
def parse_meta
|
145
|
+
# Make sure the meta isn't too large
|
146
|
+
if @meta.bytesize > MAX_META_BYTESIZE
|
147
|
+
raise InvalidMetaError,
|
148
|
+
"The server sent a meta that was too large, should be #{MAX_META_BYTESIZE} bytes, instead is #{@meta.bytesize} bytes"
|
149
|
+
end
|
150
|
+
|
151
|
+
if @status_maj == TEMPORARY_FAILURE || @status_maj == PERMANENT_FAILURE || @status_maj == REQUEST_CERTIFICATE
|
152
|
+
return
|
153
|
+
end
|
154
|
+
|
155
|
+
# Make sure meta exists (i.e. has length)
|
156
|
+
# This satisfies the INPUT and SUCCESS
|
157
|
+
unless @meta.length.positive?
|
158
|
+
raise InvalidMetaError,
|
159
|
+
"The server sent an empty meta, should've sent a user prompt"
|
160
|
+
end
|
161
|
+
|
162
|
+
# TODO: Possibly check for valid MIME type for the SUCCESS response
|
163
|
+
return if @status_maj == INPUT || @status_maj == SUCCESS
|
164
|
+
|
165
|
+
# The meta needs a specific URI if @status_maj == REDIRECT
|
166
|
+
uri = URI(@meta)
|
167
|
+
|
168
|
+
# Make sure the URI has a scheme
|
169
|
+
raise InvalidRedirectError, "The redirect link does not have a scheme" if uri.scheme.nil? || uri.scheme.empty?
|
170
|
+
|
171
|
+
# Make sure the URI scheme is 'gemini'
|
172
|
+
return if uri.scheme == "gemini"
|
173
|
+
|
174
|
+
raise InvalidRedirectError,
|
175
|
+
"The redirect link has an invalid scheme (has: #{uri.scheme}, wants: gemini)"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
module Tofu
|
5
|
+
# Stoes an SSLSocket for making requests and receiving data.
|
6
|
+
class Socket
|
7
|
+
# Constructor for the socket type.
|
8
|
+
# @param host [String] Hostname of the server to connect to.
|
9
|
+
# @param port [Integer] Server port to connect to (typically 1965).
|
10
|
+
def initialize(host, port)
|
11
|
+
@host = host
|
12
|
+
@port = port
|
13
|
+
@sock = OpenSSL::SSL::SSLSocket.open(@host, @port, context: generate_secure_context)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Open a connection to the server.
|
17
|
+
def connect
|
18
|
+
@sock.hostname = @host
|
19
|
+
@sock.connect
|
20
|
+
end
|
21
|
+
|
22
|
+
# Try and retrieve data from a request.
|
23
|
+
def gets(req)
|
24
|
+
@sock.puts req.format
|
25
|
+
|
26
|
+
io = StringIO.new
|
27
|
+
while (line = @sock.gets)
|
28
|
+
io.puts line
|
29
|
+
end
|
30
|
+
|
31
|
+
Response.new(io.string)
|
32
|
+
ensure
|
33
|
+
io.close
|
34
|
+
end
|
35
|
+
|
36
|
+
# Close the connection with the server.
|
37
|
+
def close
|
38
|
+
@sock.close
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# Configure the TLS security options to use on the socket.
|
44
|
+
def generate_secure_context
|
45
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
46
|
+
ctx.verify_hostname = true
|
47
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
48
|
+
ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
|
49
|
+
ctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF
|
50
|
+
ctx
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/net/tofu.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tofu/error"
|
4
|
+
require_relative "tofu/request"
|
5
|
+
require_relative "tofu/response"
|
6
|
+
require_relative "tofu/socket"
|
7
|
+
require_relative "tofu/version"
|
8
|
+
|
9
|
+
require "openssl"
|
10
|
+
require "stringio"
|
11
|
+
require "uri/gemini"
|
12
|
+
|
13
|
+
module Net
|
14
|
+
# Top level module for Geminispace requests.
|
15
|
+
module Tofu
|
16
|
+
def self.get(uri, trust: true)
|
17
|
+
req = Request.new(uri)
|
18
|
+
req.gets
|
19
|
+
|
20
|
+
return req.resp.body if req.resp.success?
|
21
|
+
|
22
|
+
req.resp.meta
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get_response(uri, trust: true)
|
26
|
+
req = Request.new(uri)
|
27
|
+
req.gets
|
28
|
+
|
29
|
+
req.resp
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/uri/gemini.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# MIT License
|
4
|
+
#
|
5
|
+
# Copyright (c) 2020 Étienne Deparis
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
12
|
+
# furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included in all
|
15
|
+
# copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
# SOFTWARE.
|
24
|
+
|
25
|
+
require "uri"
|
26
|
+
|
27
|
+
module URI # :nodoc:
|
28
|
+
#
|
29
|
+
# The syntax of Gemini URIs is defined in the Gemini specification,
|
30
|
+
# section 1.2.
|
31
|
+
#
|
32
|
+
# @see https://gemini.circumlunar.space/docs/specification.html
|
33
|
+
#
|
34
|
+
class Gemini < HTTP
|
35
|
+
# A Default port of 1965 for URI::Gemini.
|
36
|
+
DEFAULT_PORT = 1965
|
37
|
+
|
38
|
+
# An Array of the available components for URI::Gemini.
|
39
|
+
COMPONENT = %i[scheme host port
|
40
|
+
path query fragment].freeze
|
41
|
+
end
|
42
|
+
|
43
|
+
if respond_to? :register_scheme
|
44
|
+
# Introduced somewhere in ruby 3.0.x
|
45
|
+
register_scheme "GEMINI", Gemini
|
46
|
+
else
|
47
|
+
@@schemes["GEMINI"] = Gemini
|
48
|
+
end
|
49
|
+
end
|
data/sig/opentofu.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: opentofu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rory Dudley
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-07-27 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |2
|
14
|
+
OpenTOFU is a client and certificate pinning library for Geminispace, derived from
|
15
|
+
the 'trust on first use' authentication scheme (https://en.wikipedia.org/wiki/Trust_on_first_use).
|
16
|
+
email:
|
17
|
+
- rory.dudley@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- ".rubocop.yml"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/net/tofu.rb
|
28
|
+
- lib/net/tofu/error.rb
|
29
|
+
- lib/net/tofu/request.rb
|
30
|
+
- lib/net/tofu/response.rb
|
31
|
+
- lib/net/tofu/socket.rb
|
32
|
+
- lib/net/tofu/version.rb
|
33
|
+
- lib/uri/gemini.rb
|
34
|
+
- sig/opentofu.rbs
|
35
|
+
homepage: https://github.com/pinecat/opentofu
|
36
|
+
licenses:
|
37
|
+
- MIT
|
38
|
+
metadata:
|
39
|
+
allowed_push_host: https://rubygems.org
|
40
|
+
homepage_uri: https://github.com/pinecat/opentofu
|
41
|
+
source_code_uri: https://github.com/pinecat/opentofu
|
42
|
+
changelog_uri: https://github.com/pinecat/opentofu
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 2.6.0
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubygems_version: 3.4.17
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: A Gemini client and certificate manager.
|
62
|
+
test_files: []
|