gack 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/README.md +53 -0
- data/gack.gemspec +24 -0
- data/lib/gack.rb +9 -0
- data/lib/gack/capsule.rb +49 -0
- data/lib/gack/logger.rb +7 -0
- data/lib/gack/request.rb +54 -0
- data/lib/gack/response.rb +42 -0
- data/lib/gack/response/status_codes.rb +59 -0
- data/lib/gack/server.rb +70 -0
- data/lib/gack/sphere.rb +30 -0
- data/lib/gack/version.rb +5 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 332de1b51ed8daf7f6d759c03df4140612328e83b107aa51accc8a7f09097377
|
4
|
+
data.tar.gz: b4b74b3ce6ae1ded331c7ecc0799b3e40e86a98b068c15dc967e208e1b97b30c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dc0d59152ad07d7955109438e174e27bf408d6872d864aa46fff496a5d4c165505460a90110bf09d81411b604bd818357977d8483580d5fe8c170de8d19bfadf
|
7
|
+
data.tar.gz: 177c947803a4584279d15135e961115f5f2d64cbfbebcf7dd2efa7ecdbf6c1f3d1cf200ee3dd5bc5748855db17ec7ec4532e4102cee24628ba658c93a0b81a60
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Gack
|
2
|
+
|
3
|
+
Gack helps you build [Gemini](https://gemini.circumlunar.space/) protocol applications with Ruby.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
Install gem:
|
8
|
+
|
9
|
+
gem install gack
|
10
|
+
|
11
|
+
See examples for more details: [Examples](./examples/)
|
12
|
+
|
13
|
+
### TLS
|
14
|
+
|
15
|
+
Gemini requires TLS handshakes. Gack does not do TLS handshakes. You need to setup a TLS terminator. I use Nginx. Below is some quick information on how you might begin to use Nginx to terminate TLS.
|
16
|
+
|
17
|
+
#### Certs
|
18
|
+
|
19
|
+
Generate a self-signed `.crt` and `.key`. Example:
|
20
|
+
|
21
|
+
openssl req -newkey rsa:2048 -nodes -keyout localhost.key -nodes -x509 -out localhost.crt -subj "/CN=localhost"
|
22
|
+
|
23
|
+
#### Nginx
|
24
|
+
|
25
|
+
Nginx will be used for the TLS handshake and termination:
|
26
|
+
|
27
|
+
stream {
|
28
|
+
|
29
|
+
upstream backend {
|
30
|
+
# change this to reflect what port your Gack server is running on (default is 6565)
|
31
|
+
server localhost:6565;
|
32
|
+
}
|
33
|
+
|
34
|
+
server {
|
35
|
+
# 195 is the default Gemini protocol port
|
36
|
+
listen 1965 ssl;
|
37
|
+
proxy_pass backend;
|
38
|
+
|
39
|
+
# change these to match the directory and filename of your crt and key
|
40
|
+
ssl_certificate /your/localtion/to/certs/localhost.crt;
|
41
|
+
ssl_certificate_key /your/localtion/to/localhost.key;
|
42
|
+
|
43
|
+
ssl_protocols TLSv1.1 TLSv1.2;
|
44
|
+
|
45
|
+
ssl_prefer_server_ciphers on;
|
46
|
+
ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
|
47
|
+
|
48
|
+
ssl_session_cache shared:SSL:20m;
|
49
|
+
ssl_session_timeout 4h;
|
50
|
+
|
51
|
+
ssl_handshake_timeout 20s;
|
52
|
+
}
|
53
|
+
}
|
data/gack.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/gack/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'gack'
|
7
|
+
s.version = Gack::VERSION.dup
|
8
|
+
s.date = '2020-11-10'
|
9
|
+
s.summary = 'Basic Gemini server'
|
10
|
+
s.description = 'Gack helps you build Gemini protocol applications with Ruby.'
|
11
|
+
s.authors = ['Robert Peterson']
|
12
|
+
s.email = 'me@robertp.me'
|
13
|
+
s.files = Dir['README.md', 'gack.gemspec', 'lib/**/*']
|
14
|
+
s.homepage = 'https://github.com/rawburt/gack'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.required_ruby_version = '>= 2.6.0'
|
18
|
+
|
19
|
+
s.add_development_dependency 'bundler', '~> 2.1'
|
20
|
+
s.add_development_dependency 'rspec', '~> 3.10'
|
21
|
+
s.add_development_dependency 'rubocop', '~> 1.2'
|
22
|
+
s.add_development_dependency 'rubocop-rspec', '~> 2.0'
|
23
|
+
s.add_development_dependency 'simplecov', '~> 0.19'
|
24
|
+
end
|
data/lib/gack.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'gack/version'
|
4
|
+
require_relative 'gack/logger'
|
5
|
+
require_relative 'gack/request'
|
6
|
+
require_relative 'gack/response'
|
7
|
+
require_relative 'gack/server'
|
8
|
+
require_relative 'gack/sphere'
|
9
|
+
require_relative 'gack/capsule'
|
data/lib/gack/capsule.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gack
|
4
|
+
# The main DSL for making Gemini apps with Gack
|
5
|
+
class Capsule
|
6
|
+
def self.sphere(path, &handler)
|
7
|
+
spheres << Gack::Sphere.new(path, &handler)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.spheres
|
11
|
+
@spheres ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.run!
|
15
|
+
new(spheres).run!
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :spheres
|
19
|
+
|
20
|
+
def initialize(spheres)
|
21
|
+
@spheres = spheres
|
22
|
+
end
|
23
|
+
|
24
|
+
def run!
|
25
|
+
server = Gack::Server.new
|
26
|
+
server.event_loop do |request|
|
27
|
+
server_loop_handler(request)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def server_loop_handler(request)
|
32
|
+
sphere = match_sphere(request.location)
|
33
|
+
if sphere
|
34
|
+
result = sphere.handle_request(request)
|
35
|
+
if result.is_a?(Response)
|
36
|
+
result
|
37
|
+
else
|
38
|
+
Response.new(Response::StatusCodes::SUCCESS, Response::MIME[:text], result)
|
39
|
+
end
|
40
|
+
else
|
41
|
+
Response.new(Response::StatusCodes::NOT_FOUND)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def match_sphere(location)
|
46
|
+
spheres.find { |s| s.path_match?(location) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/gack/logger.rb
ADDED
data/lib/gack/request.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gack
|
4
|
+
# Parse a Gemini request into a friendly object
|
5
|
+
class Request
|
6
|
+
PROTOCOL = 'gemini://'
|
7
|
+
|
8
|
+
attr_reader :request
|
9
|
+
|
10
|
+
def initialize(request)
|
11
|
+
@request = request
|
12
|
+
end
|
13
|
+
|
14
|
+
def location
|
15
|
+
@location ||= parse_location
|
16
|
+
end
|
17
|
+
|
18
|
+
def input
|
19
|
+
@input ||= parse_input
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parse_location
|
25
|
+
# ignore input
|
26
|
+
location_only = clean_request.split('?').first
|
27
|
+
|
28
|
+
return unless location_only
|
29
|
+
|
30
|
+
split_request = location_only.split('/')
|
31
|
+
|
32
|
+
return '/' if split_request.size == 1
|
33
|
+
|
34
|
+
return "/#{split_request.last}" if split_request.size == 2
|
35
|
+
|
36
|
+
"/#{split_request[1..-1].join('/')}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_input
|
40
|
+
split_input = clean_request.split('?')
|
41
|
+
|
42
|
+
if split_input.size > 2
|
43
|
+
CGI.unescape(split_input[1..-1].join('?'))
|
44
|
+
else
|
45
|
+
CGI.unescape(split_input.last)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# `.chomp` to drop the trailing \r\n and `.gsub` to remove the protocol
|
50
|
+
def clean_request
|
51
|
+
@clean_request ||= request.chomp.gsub(PROTOCOL, '')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'response/status_codes'
|
4
|
+
|
5
|
+
module Gack
|
6
|
+
# Build a Gemini response
|
7
|
+
class Response
|
8
|
+
InvalidStatusCodeError = Class.new(StandardError)
|
9
|
+
BodyNotAllowedError = Class.new(StandardError)
|
10
|
+
MetaTooLongError = Class.new(StandardError)
|
11
|
+
InvalidMimeError = Class.new(StandardError)
|
12
|
+
|
13
|
+
META_MAX_BYTES = 1024
|
14
|
+
|
15
|
+
MIME = {
|
16
|
+
text: 'text/gemini'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
attr_reader :status_code, :meta, :body
|
20
|
+
|
21
|
+
def initialize(status_code, meta = nil, body = nil)
|
22
|
+
@status_code = Integer(status_code)
|
23
|
+
|
24
|
+
raise InvalidStatusCodeError unless StatusCodes::VALID_CODES.include?(@status_code)
|
25
|
+
|
26
|
+
@meta = meta
|
27
|
+
|
28
|
+
raise MetaTooLongError if @meta && @meta.bytesize > META_MAX_BYTES
|
29
|
+
raise InvalidMimeError if @status_code == StatusCodes::SUCCESS && @meta.nil?
|
30
|
+
|
31
|
+
@body = body
|
32
|
+
|
33
|
+
raise BodyNotAllowedError if @body && @status_code != StatusCodes::SUCCESS
|
34
|
+
end
|
35
|
+
|
36
|
+
def finalize
|
37
|
+
return "#{status_code}\r\n" unless meta
|
38
|
+
|
39
|
+
"#{status_code} #{meta}\r\n#{body}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gack
|
4
|
+
class Response
|
5
|
+
# Gemini Response Status Codes
|
6
|
+
class StatusCodes
|
7
|
+
# Inputs. <META> = prompt text
|
8
|
+
INPUT = 10
|
9
|
+
SENSITIVE_INPUT = 11
|
10
|
+
|
11
|
+
# Success. <META> = MIME. <BODY>
|
12
|
+
SUCCESS = 20
|
13
|
+
|
14
|
+
# Redirect. <META> = URL
|
15
|
+
REDIRECT_TEMPORARY = 30
|
16
|
+
REDIRECT_PERMANANT = 31
|
17
|
+
|
18
|
+
# Temporary Failure. <META> = additional information
|
19
|
+
TEMPORARY_FAILURE = 40
|
20
|
+
SERVER_UNAVAILABLE = 41
|
21
|
+
CGI_ERROR = 42
|
22
|
+
PROXY_ERROR = 43
|
23
|
+
SLOW_DOWN = 44
|
24
|
+
|
25
|
+
# Permanent Failure. <META> = additional information
|
26
|
+
PERMANENT_FAILURE = 50
|
27
|
+
NOT_FOUND = 51
|
28
|
+
GONE = 52
|
29
|
+
PROXY_REQUEST_REFUSED = 53
|
30
|
+
BAD_REQUEST = 59
|
31
|
+
|
32
|
+
# Client Certificate Requires. <META> = additional information
|
33
|
+
CLIENT_CERTIFICATE_REQUIRED = 60
|
34
|
+
CERTIFICATE_NOT_AUTHORIZED = 61
|
35
|
+
CERTIFICATE_NOT_VALID = 62
|
36
|
+
|
37
|
+
VALID_CODES = [
|
38
|
+
INPUT,
|
39
|
+
SENSITIVE_INPUT,
|
40
|
+
SUCCESS,
|
41
|
+
REDIRECT_TEMPORARY,
|
42
|
+
REDIRECT_PERMANANT,
|
43
|
+
TEMPORARY_FAILURE,
|
44
|
+
SERVER_UNAVAILABLE,
|
45
|
+
CGI_ERROR,
|
46
|
+
PROXY_ERROR,
|
47
|
+
SLOW_DOWN,
|
48
|
+
PERMANENT_FAILURE,
|
49
|
+
NOT_FOUND,
|
50
|
+
GONE,
|
51
|
+
PROXY_REQUEST_REFUSED,
|
52
|
+
BAD_REQUEST,
|
53
|
+
CLIENT_CERTIFICATE_REQUIRED,
|
54
|
+
CERTIFICATE_NOT_AUTHORIZED,
|
55
|
+
CERTIFICATE_NOT_VALID
|
56
|
+
].freeze
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/gack/server.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module Gack
|
6
|
+
# The main TCP server
|
7
|
+
class Server
|
8
|
+
attr_reader :tcp, :port, :logger
|
9
|
+
|
10
|
+
def initialize(tcp: TCPServer, port: 6565, logger: Gack::Logger)
|
11
|
+
@tcp = tcp
|
12
|
+
@port = port
|
13
|
+
@logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
def event_loop(&blk)
|
17
|
+
logger.info("starting. port=#{port}")
|
18
|
+
|
19
|
+
server = tcp.new(port)
|
20
|
+
|
21
|
+
loop do
|
22
|
+
Thread.new(server.accept) do |client|
|
23
|
+
main_handler(client, &blk)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
rescue StandardError => e
|
27
|
+
logger.error(e)
|
28
|
+
end
|
29
|
+
|
30
|
+
def main_handler(client, &blk)
|
31
|
+
unrescued_handler(client, &blk)
|
32
|
+
rescue StandardError => e
|
33
|
+
logger.error(e)
|
34
|
+
|
35
|
+
failure_response.finalize
|
36
|
+
end
|
37
|
+
|
38
|
+
def unrescued_handler(client, &blk)
|
39
|
+
raw_request = receive_request(client)
|
40
|
+
|
41
|
+
request = Request.new(raw_request)
|
42
|
+
|
43
|
+
logger.info("request received: #{request.location}")
|
44
|
+
|
45
|
+
response = blk.call(request)
|
46
|
+
|
47
|
+
logger.info("response: #{response.status_code}")
|
48
|
+
|
49
|
+
client.puts(response.finalize)
|
50
|
+
client.close
|
51
|
+
end
|
52
|
+
|
53
|
+
def failure_response
|
54
|
+
Response.new(Response::StatusCodes::TEMPORARY_FAILURE, 'Server Error')
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def receive_request(client)
|
60
|
+
request = ''
|
61
|
+
begin
|
62
|
+
client.read_nonblock(2056, request)
|
63
|
+
rescue IO::WaitReadable
|
64
|
+
IO.select([client])
|
65
|
+
retry
|
66
|
+
end
|
67
|
+
request
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/gack/sphere.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gack
|
4
|
+
# Sphere is a Gemini request handler wrapper for a given path
|
5
|
+
class Sphere
|
6
|
+
HandlerMissingError = Class.new(StandardError)
|
7
|
+
|
8
|
+
attr_reader :path, :handler
|
9
|
+
|
10
|
+
def initialize(path, &handler)
|
11
|
+
@path = path
|
12
|
+
|
13
|
+
raise HandlerMissingError unless handler
|
14
|
+
|
15
|
+
@handler = handler
|
16
|
+
end
|
17
|
+
|
18
|
+
def path_match?(string)
|
19
|
+
if path.is_a?(Regexp)
|
20
|
+
path.match?(string)
|
21
|
+
else
|
22
|
+
path.eql?(string)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_request(request)
|
27
|
+
handler.call(request)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/gack/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Peterson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.10'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.10'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.2'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.19'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.19'
|
83
|
+
description: Gack helps you build Gemini protocol applications with Ruby.
|
84
|
+
email: me@robertp.me
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files: []
|
88
|
+
files:
|
89
|
+
- README.md
|
90
|
+
- gack.gemspec
|
91
|
+
- lib/gack.rb
|
92
|
+
- lib/gack/capsule.rb
|
93
|
+
- lib/gack/logger.rb
|
94
|
+
- lib/gack/request.rb
|
95
|
+
- lib/gack/response.rb
|
96
|
+
- lib/gack/response/status_codes.rb
|
97
|
+
- lib/gack/server.rb
|
98
|
+
- lib/gack/sphere.rb
|
99
|
+
- lib/gack/version.rb
|
100
|
+
homepage: https://github.com/rawburt/gack
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 2.6.0
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubygems_version: 3.0.3
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: Basic Gemini server
|
123
|
+
test_files: []
|