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.
@@ -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
@@ -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
+ }
@@ -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
@@ -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'
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Gack
6
+ Logger = Logger.new($stdout)
7
+ end
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gack
4
+ VERSION = '0.0.1'
5
+ end
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: []