gack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []