koi-server 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1ae15672990b7868474ae97997cb5938dfdc0bae534de6568749aad8ad1f111f
4
+ data.tar.gz: 3889154d8e5bf470942ef2af98657c3c4cf7b24561c8a11428b903c3108ad692
5
+ SHA512:
6
+ metadata.gz: 31e9220a59089665ec0bcdd807ab2b87ddd749e3e52b9ba818e6f5ef564a4bd940ad4f4520e000638670d2419f4d5445d8e8bf67423d79f40e33714589ff6224
7
+ data.tar.gz: 044e02890146c4c86aa7b397b14d79acf3bcf9398f38cf61d5566ad473a3b99a78f3ecccc086b1a7e2017de1697da8500d4a9c1d6484f9bc8ba02b19ecf1c62f
data/.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'koi-server'
3
+ s.version = '1.0.0'
4
+ s.licenses = ['MIT']
5
+ s.summary = "Koi is a ruby-based Gemini server that supports ERB templating."
6
+ s.description = "Koi is a ruby-based Gemini server that supports .erb templating, and static files, and handles TLS certs for you.
7
+
8
+ Koi is designed to make hosting Gemini CGI pages a joyful and
9
+ tranquil experience, uniting the conceptual beauty of a simple web
10
+ with developer happiness.
11
+
12
+ By using a combination of Ruby powered ERB templating and
13
+ traditional static file serving we distill a solution offering
14
+ the best of both worlds.
15
+ "
16
+ s.authors = ["Justin Woodring"]
17
+ s.email = 'jwoodrg@gmail.com'
18
+ s.files = `git ls-files -z`.split("\x0")
19
+ s.executables << "koi"
20
+ s.homepage = 'https://rubygems.org/gems/koi'
21
+ s.metadata = { "source_code_uri" => "https://github.com/JustinWoodring/Koi" }
22
+ end
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ *~
2
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # gem "rails"
data/README.org ADDED
@@ -0,0 +1,103 @@
1
+ #+title: Koi Gemini Server
2
+ #+subtitle: A Gemini server that serves templated static .erb files.
3
+ #+author: Justin Woodring
4
+ #+language: en
5
+
6
+ Koi is another ruby-based Gemini server that supports .erb templating,
7
+ and static files, and handles TLS certs for you.
8
+
9
+ * Ethos
10
+
11
+ Koi is designed to make hosting Gemini CGI pages a joyful and
12
+ tranquil experience, uniting the conceptual beauty of a simple web
13
+ with developer happiness.
14
+
15
+ By using a combination of Ruby powered ERB templating and
16
+ traditional static file serving we distill a solution offering
17
+ the best of both worlds.
18
+
19
+ * How it works
20
+
21
+ This server operates in every capacity as a static file server,
22
+ unless the file it is serving begins with . i.e. hidden files or
23
+ the file ends a .erb extension.
24
+
25
+ Files ending with a .erb extension use the ERB markup syntax to
26
+ generate responses that are assumed to be `gemtext`, under most
27
+ circumstances. That said, each file is executed with it's directory
28
+ as the PWD and it injects binds the following into the ERB environment:
29
+
30
+ ** Request: Object = Gemini Request Received
31
+ *** protocol: String - should always be 'gemini://'
32
+ *** domain: String - the domain name in the absolute URI
33
+ *** path: String - absolute file path relative to served root
34
+ *** query: String - what follows ? in the absolute URI (url_encoded)
35
+
36
+ ** response_status: Integer = nil
37
+ These object can be set to override default gemtext generation. This
38
+ particular property changes the response code with the raw Gemini code
39
+ for a given response e.g. 10 is input required.
40
+
41
+ ** response_metadata: String = nil
42
+ This is the metadata object of the response that can be overridden to
43
+ supply supplementary info for a given response. e.g. a redirect URL,
44
+ a new content type header, a query description text.
45
+
46
+ ** response_content: String = nil
47
+ This can be used to return arbitrary content and overrides what .erb
48
+ evaluates to if set. You could theoretically generate and serve images
49
+ from an .erb by setting this property with the correct mime type
50
+ set in the metadata.
51
+
52
+ Here is a contrived example that demonstates the power of ERB templating
53
+ in this context:
54
+
55
+ #+NAME: /posts/index.erb
56
+ #+BEGIN_SRC erb
57
+ # Search my posts
58
+
59
+ <% if request.query == "" %>
60
+ <% @response_status = 10 %>
61
+ <% @response_metadata = "Enter a filter term" %>
62
+ <% end %>
63
+
64
+ <% `ls`.split("\n").select {|x| x.start_with?(@request.query) and x.end_with?(".gmi")}.each do |item| %>
65
+ => gemini://mysite.com/posts/<%= item %>
66
+ <% end %>
67
+ #+END_SRC
68
+
69
+ * Running Your Own Server
70
+
71
+ Running your own server is as simple as installing one gem: `koi`
72
+ and pointing it at a cert and key that you might even generate
73
+ using `koi`.
74
+
75
+ #+begin_example
76
+
77
+ # Defaults are -s nil -b 0.0.0.0 -p 1965 -e (.erb enabled)
78
+
79
+ koi -s /home/user/public_gmi
80
+
81
+ OR
82
+
83
+ koi -s /home/user/public_gmi -b 0.0.0.0 -p 1965
84
+
85
+ OR
86
+
87
+ koi -s /home/user/public_gmi -b 0.0.0.0 -p 1965 --no-erb
88
+
89
+ OR
90
+
91
+ koi -s /home/user/public_gmi -b 0.0.0.0 -p 1965 -e -k key.pem -c cert.pem
92
+
93
+ FOR CERTIFICATE GENERATION
94
+
95
+ koi -g COMMONNAME
96
+ #+end_example
97
+
98
+
99
+ * No AI Notice
100
+
101
+ No AI tools were used at any point during developemnt or
102
+ documentation. This project was expressly 100% hand-written
103
+ by a human.
data/bin/koi ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative '../lib/koi.rb'
6
+
7
+ # Declare defaults
8
+ config = {
9
+ bind_addr: "0.0.0.0", # Bind All Available Interfaces
10
+ bind_port: "1965", # Default Gemini Port
11
+ file_root_dir: nil, # No Default
12
+ erb_enabled: true # Assume Yes
13
+ key_file_path: nil
14
+ cert_file_path: nil
15
+ }
16
+
17
+
18
+ # Declare parse options
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = "Usage:\tkoi [options]\n\tkoi -s ~/public_gmi -b '0.0.0.0' -p 1965 -e true"
21
+
22
+ opts.separator ""
23
+ opts.separator "Specific options:"
24
+
25
+ opts.on("-s", "--serve FILE_ROOT", String, "Required. File root of public .gmi and .erb files") do |v|
26
+ config[:file_root_dir] = v
27
+ end
28
+
29
+ opts.on("-b", "--bind_addr ADDR", String, "Bind address for the server to listen on") do |v|
30
+ config[:bind_addr] = v
31
+ end
32
+
33
+ opts.on("-p", "--port PORT", Integer, "Bind port for the server to listen on") do |v|
34
+ config[:bind_port] = v
35
+ end
36
+
37
+ opts.on("-e", "--[no]-erb", "Enable serving .erb files") do |v|
38
+ config[:erb_enabled] = v
39
+ end
40
+
41
+ opts.separator "Certificate options, if either is unset the server will generate a throwaway self-signed cert on-demand."
42
+
43
+ opts.on("-k", "--key KEY_FILE_PATH", String, "Path to your private key.") do |v|
44
+ config[:key_file_path] = v
45
+ end
46
+
47
+ opts.on("-c", "--cert CERT_FILE_PATH", String, "Path to your certificate.") do |v|
48
+ config[:cert_file_path] = v
49
+ end
50
+
51
+ opts.separator ""
52
+ opts.separator "Common options:"
53
+
54
+ opts.on_tail("-g", "--generate-cert CN", "Generate a self-signed certficate") do |cn|
55
+ key, cert = Koi::SSL.generate_key_and_cert(cn)
56
+ File.write('koi_private_key.pem', key.to_pem)
57
+ File.write('koi_certificate.pem', cert.to_pem)
58
+
59
+ puts "Key written to koi_private_key.pem successfully!"
60
+ puts "Key written to koi_certificate.pem successfully!"
61
+ exit
62
+ end
63
+
64
+ opts.on_tail("-h", "--help", "Prints this help") do
65
+ puts parser
66
+ exit
67
+ end
68
+ end
69
+
70
+ # Parse output and validate required fields.
71
+ puts parser.parse!(ARGV)
72
+
73
+ if config[:file_root_dir] == nil then
74
+ puts parser
75
+ exit
76
+ end
77
+
78
+ # Run the Koi server!
79
+
80
+ koi = Koi::Pond.new(config)
81
+ koi.run
82
+
83
+
84
+
85
+
86
+
87
+
@@ -0,0 +1,118 @@
1
+ require 'erb'
2
+ require 'open3'
3
+
4
+ require_relative '../gemini'
5
+
6
+ class Koi::ERB::Handler
7
+ def initialize
8
+ # We might put properties that can defined here in the future.
9
+ end
10
+
11
+ def handler_internal(filename, request)
12
+ ruby_code = <<~RUBY
13
+ require 'erb'
14
+
15
+ class Koi
16
+ end
17
+
18
+ class Koi::ERB
19
+ end
20
+
21
+ class Koi::ERB::Context
22
+ @request = nil
23
+ @response_status = nil
24
+ @response_metadata = nil
25
+ @response_content = nil
26
+
27
+ attr_accessor :request, :response_status, :response_metadata, :response_content
28
+
29
+ def initialize(request)
30
+ @request = request
31
+ @response_status = nil
32
+ @response_metadata = nil
33
+ @response_content = nil
34
+ end
35
+
36
+ def get_binding
37
+ return binding
38
+ end
39
+ end
40
+
41
+ class Koi::Gemini
42
+ end
43
+
44
+ class Koi::Gemini::Request
45
+ attr_accessor :is_valid, :protocol, :domain, :path, :query
46
+
47
+ def initialize(is_valid, protocol, domain, path, query)
48
+ @is_valid = is_valid
49
+ @protocol = protocol
50
+ @domain = domain
51
+ @path = path
52
+ @query = query
53
+ end
54
+ end
55
+
56
+ request = Koi::Gemini::Request.new(
57
+ "#{request.is_valid}",
58
+ '#{request.protocol.to_s.dump}'.undump,
59
+ '#{request.domain.to_s.dump}'.undump,
60
+ '#{request.path.to_s.dump}'.undump,
61
+ '#{request.query.to_s.dump}'.undump)
62
+
63
+ template_content = File.binread('#{filename.dump}'.undump)
64
+ context = Koi::ERB::Context.new(request)
65
+ erb = ERB.new(template_content)
66
+
67
+ output = erb.result(context.get_binding)
68
+
69
+ status = if context.response_status != nil
70
+ context.response_status else 20 end
71
+ metadata = if context.response_metadata != nil
72
+ context.response_metadata else
73
+ "text/gemini; charset=utf-8" end
74
+ content = if context.response_content != nil
75
+ context.response_content else
76
+ output.b end
77
+
78
+ puts status
79
+ puts metadata
80
+ puts content
81
+ RUBY
82
+
83
+ return ruby_code
84
+ end
85
+
86
+ def handle(valid_path, request)
87
+ begin
88
+ # Initialize ERB
89
+ filename = valid_path[valid_path.rindex('/')+1..-1]
90
+ path_prefix = valid_path[0..valid_path.rindex('/')]
91
+
92
+ stdout, stderr, code = Open3.capture3("ruby",
93
+ chdir: valid_path[0..valid_path.rindex('/')],
94
+ stdin_data: handler_internal(filename, request))
95
+
96
+ output = stdout.split("\n",3)
97
+
98
+ puts "---\n#{stdout}\n---"
99
+ puts "---\n#{stderr}\n---"
100
+
101
+ status = output[0]
102
+ metadata = output[1]
103
+ content = output[2]
104
+
105
+ return Koi::Gemini::Response.new(status, metadata, content)
106
+ rescue Exception => e
107
+ status = Koi::Gemini::Status::PermanentGenericError
108
+ metadata = "CGI backend experienced an error."
109
+ content = nil
110
+
111
+ puts e
112
+
113
+ return Koi::Gemini::Response.new(status, metadata, content)
114
+ end
115
+ end
116
+ end
117
+
118
+
data/lib/koi/erb.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'erb'
2
+
3
+ class Koi::ERB
4
+ end
5
+
6
+ require_relative './erb/handler'
data/lib/koi/file.rb ADDED
@@ -0,0 +1,95 @@
1
+ require_relative "./gemini"
2
+ require_relative "./erb"
3
+
4
+ class FileServer
5
+ @base_directory = nil
6
+ @erb_enabled = false
7
+ @erb_handler = nil
8
+
9
+ def initialize(base_directory, erb_enabled)
10
+ @base_directory = base_directory
11
+ @erb_enabled = erb_enabled
12
+ if erb_enabled then
13
+ @erb_handler = Koi::ERB::Handler.new
14
+ end
15
+ end
16
+
17
+ def search_path(path)
18
+ files = Dir.glob("#{@base_directory}/**/*").each do |f|
19
+ f.sub!("#{@base_directory}", "")
20
+ end.append("/")
21
+
22
+ f_trimmed = files.find { |file| path == file }
23
+
24
+ if f_trimmed != nil then
25
+ if File.directory?(File.join("#{@base_directory}", f_trimmed)) then
26
+ # If we are in a directory lets test for an index.erb and then index.gmi
27
+
28
+ if @erb_enabled then
29
+ i = File.join("#{@base_directory}", f_trimmed, "index.erb")
30
+
31
+ if File.exist?(i) then
32
+ return i
33
+ end
34
+ end
35
+
36
+ i = File.join("#{@base_directory}", f_trimmed, "index.gmi")
37
+ if File.exist?(i) then
38
+ return i
39
+ end
40
+ else
41
+ i = File.join("#{@base_directory}", f_trimmed)
42
+ return i
43
+ end
44
+ end
45
+
46
+ return nil
47
+ end
48
+
49
+ def route(request)
50
+ valid_path = search_path(request.path)
51
+
52
+ if valid_path != nil then
53
+ if valid_path.end_with?(".erb") and not @erb_enabled then
54
+ # Filter a file ending in .erb if .erb is not enabled. If the path ends with this return Not Found.
55
+ return Koi::Gemini::Response.new(Koi::Gemini::Status::NotFound, "Resource Not Found", nil)
56
+ elsif valid_path.split("/")[-1].start_with?(".") then
57
+ # Check for files beginning with . and ignore them.
58
+
59
+ # If the file begins with . as in .env then return Not Found.
60
+ return Koi::Gemini::Response.new(Koi::Gemini::Status::NotFound, "Resource Not Found", nil)
61
+ else
62
+ # Consider the route safe, figure out how to pack the response.
63
+ if valid_path.end_with?(".erb")
64
+ response = @erb_handler.handle(valid_path, request)
65
+ return response
66
+ else
67
+ status = Koi::Gemini::Status::Success
68
+ metadata = if valid_path.end_with?(".gmi")
69
+ "text/gemini; charset=utf-8"
70
+ elsif valid_path.end_with?(".txt")
71
+ "text/plain; charset=utf-8"
72
+ elsif valid_path.end_with?(".zip")
73
+ "application/zip"
74
+ elsif valid_path.end_with?(".pdf")
75
+ "application/pdf"
76
+ elsif valid_path.end_with?(".gif")
77
+ "image/gif"
78
+ elsif valid_path.end_with?(".jpeg", ".jpg")
79
+ "image/jpeg"
80
+ elsif valid_path.end_with?(".png")
81
+ "image/png"
82
+ else
83
+ "application/octet-stream"
84
+ end
85
+
86
+ content = File.binread(valid_path)
87
+ return response = Koi::Gemini::Response.new(status, metadata, content)
88
+ end
89
+ end
90
+ else
91
+ # No File Found
92
+ return Koi::Gemini::Response.new(Koi::Gemini::Status::NotFound, "Resource Not Found", nil)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,60 @@
1
+ class Koi::Gemini::Request
2
+ @is_valid = true
3
+
4
+ attr_accessor :is_valid, :protocol, :domain, :path, :query
5
+
6
+ def self.parse(input)
7
+ if self.is_a?(Class)
8
+ req = Koi::Gemini::Request.allocate
9
+
10
+ if input[0...9] == "gemini://" then
11
+ req.protocol = "gemini://"
12
+ end
13
+
14
+ domain_path_query = input[9..-1].gsub('\r\n','').strip
15
+ split = domain_path_query.split("/",2)
16
+
17
+ if split[0] != nil
18
+ and not split[0].include?("/")
19
+ and not split[0].include?('"')
20
+ and not split[0].include?("'")
21
+ and not split[0].include?(" ") then
22
+
23
+ req.domain = split[0]
24
+ req.path = "/index"
25
+ req.query = ""
26
+ req.is_valid = true
27
+ else
28
+ req.is_valid = false
29
+ end
30
+
31
+ if split[1] != nil
32
+ if not split[1].include?("..")
33
+ and not split[1].include?(" ")
34
+ and not split[1].include?("'")
35
+ and not split[1].include?('"') then
36
+ if split[1].include?("?") then
37
+ path_query = split[1].split("?")
38
+ req.path = "/"+path_query[0]
39
+ req.query = path_query[1]
40
+ else
41
+ req.path = "/"+split[1]
42
+ end
43
+ else
44
+ req.is_valid = false
45
+ end
46
+ end
47
+
48
+ return req
49
+ end
50
+ raise "This method should be called from the class, not an instance!"
51
+ end
52
+
53
+ def is_valid?
54
+ if self.is_a?(Class)
55
+ raise "This method must be called on an instance, not the class!"
56
+ end
57
+
58
+ return @is_valid
59
+ end
60
+ end
@@ -0,0 +1,15 @@
1
+ require_relative './status'
2
+
3
+ class Koi::Gemini::Response
4
+ attr_accessor :status, :metadata, :content
5
+
6
+ def initialize(status, metadata, content)
7
+ @status = status
8
+ @metadata = metadata
9
+ @content = content
10
+ end
11
+
12
+ def to_s
13
+ return "#{@status} #{metadata}\r\n#{@content}".b
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ module Koi::Gemini::Status
2
+ #1X
3
+ InputExpected = 10
4
+ InputExpectedSensitive = 11
5
+
6
+ #2X
7
+ Success = 20
8
+
9
+ #3X
10
+ TemporaryRedirection = 30
11
+ PermanentRedirection = 31
12
+
13
+ #4X
14
+ RetryableGenericError = 40
15
+ RetryableServerUnavailableError = 41
16
+ RetryableCGIError = 42
17
+ RetryableProxyError = 43
18
+ RetryableSlowDownError = 44
19
+
20
+ #5X
21
+ PermanentGenericError = 50
22
+ NotFound = 51
23
+ Gone = 52
24
+ ProxyRequestRefused = 53
25
+ BadRequest = 59
26
+
27
+ #60
28
+ NeedClientCertificate = 60
29
+ CertificateNotAuthorized = 61
30
+ CertificateNotValid = 62
31
+ end
data/lib/koi/gemini.rb ADDED
@@ -0,0 +1,6 @@
1
+ class Koi::Gemini
2
+ end
3
+
4
+ require_relative './gemini/status'
5
+ require_relative './gemini/request'
6
+ require_relative './gemini/response'
data/lib/koi/server.rb ADDED
@@ -0,0 +1,91 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+ require_relative './gemini'
4
+ require_relative './ssl'
5
+ require_relative './file'
6
+
7
+ class Koi::Server
8
+ @config = {}
9
+ @file_server = nil
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @file_server = FileServer.new(@config[:file_root_dir], @config[:erb_enabled])
14
+ end
15
+
16
+ def run
17
+ # Spawn TCP listener.
18
+ server = TCPServer.new(@config[:bind_addr], @config[:bind_port])
19
+
20
+ puts "
21
+
22
+ :.
23
+ :xx
24
+ .;+x +;+:
25
+ .x +&;;;;;;;+;;;+;:;;X :.:.
26
+ ::::+&&;::::::::::::::$+ ..:..;
27
+ ;xx::::::::::;;::::::...::+ :;:.;;
28
+ ;;::::::::;+xxxxx;::.:.. x+;+Xxx
29
+ +$::..:; .:: &$$$X;x
30
+ ++;: &&&$$x
31
+ :x: +. &$$$x
32
+ + ;;. $$XX
33
+ ;; XXx$$&X;;&+X$& ;: :X&$
34
+ ++; ++x::::::::x+&&&$$$Xxxx::::::;
35
+ +;+;; ++;:X:::::;$:;:XXXx:::xxX+xxxx;
36
+ :;;;:;. .::+X$;:$$$X+::::::x
37
+ :;;:;;: :. x.+
38
+ .:..:. . X:;
39
+ . . +++
40
+ ::
41
+
42
+
43
+ "
44
+ puts "Koi Gemini Server - #{Koi::VERSION} - Listening on #{@config[:bind_addr]}:#{@config[:bind_port]}."
45
+
46
+ # Define SSL context
47
+
48
+ ctx = if @config[:key_file_path] == nil or
49
+ @config[:cert_file_path] == nil then
50
+ Koi::SSL.generate_debug_ctx
51
+ else
52
+ Koi::SSL.generate_ctx_from_files(@config[:key_file_path], @config[:cert_file_path])
53
+ end
54
+
55
+ # Loop TCP connections with Thread pool.
56
+ loop do
57
+ Thread.start(server.accept) do |socket|
58
+ begin
59
+ # Begin client upgrade
60
+ client = OpenSSL::SSL::SSLSocket.new(socket, ctx)
61
+ client.accept
62
+
63
+ # Client stable begin server request handling
64
+ handle(client)
65
+
66
+ rescue OpenSSL::SSL::SSLError
67
+ puts "Failed to upgrade client connection to SSL."
68
+ ensure
69
+ ssl_socket.close rescue nil
70
+ socket.close rescue nil
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def handle(client)
77
+ request = Koi::Gemini::Request.parse(client.gets)
78
+
79
+ if not request.is_valid? then
80
+ resp = Koi::Gemini::Response.new(Koi::Gemini::Status::BadRequest)
81
+
82
+ puts "#{resp.status}: #{request.protocol}#{request.domain}#{request.path}" + (request.query != "" ? "?#{request.query}" : "")
83
+ client.send(resp.to_s)
84
+ else
85
+ resp = @file_server.route(request)
86
+
87
+ puts "#{resp.status}: #{request.protocol}#{request.domain}#{request.path}" + (request.query != "" ? "?#{request.query}" : "")
88
+ client.write(resp.to_s)
89
+ end
90
+ end
91
+ end
data/lib/koi/ssl.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'openssl'
2
+
3
+ class Koi::SSL
4
+
5
+ def self.generate_debug_ctx
6
+ ctx = OpenSSL::SSL::SSLContext.new
7
+
8
+ ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
9
+ ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION
10
+
11
+ ctx.key = generate_private_key
12
+ ctx.cert = generate_and_sign_cert(ctx.key, "localhost")
13
+
14
+ return ctx
15
+ end
16
+
17
+ def self.generate_ctx_from_files(key_path, cert_path)
18
+ ctx = OpenSSL::SSL::SSLContext.new
19
+ ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
20
+ ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION
21
+
22
+ # Load private key.
23
+ key_data = File.read(key_path)
24
+ private_key = OpenSSL::PKey.read(key_data, nil)
25
+
26
+ # Load the X509 certificate
27
+ cert_data = File.read(cert_path)
28
+ cert = OpenSSL::X509::Certificate.new(cert_data)
29
+
30
+ # Verify that the private key matches the certificate
31
+ if cert.check_private_key(private_key)
32
+ raise "Error: Private key does not match the certificate!"
33
+ end
34
+
35
+ ctx.key = private_key
36
+ ctx.cert = cert
37
+ return ctx
38
+ end
39
+
40
+
41
+ def self.generate_key_and_cert(cn)
42
+ key = generate_private_key
43
+ cert = generate_and_sign_cert(key, cn)
44
+
45
+ return key, cert
46
+ end
47
+
48
+ def self.generate_private_key
49
+ key = OpenSSL::PKey::RSA.new(2048)
50
+ return key
51
+ end
52
+
53
+ def self.generate_and_sign_cert(key, cn)
54
+ cert = OpenSSL::X509::Certificate.new
55
+ cert.version = 2
56
+ cert.serial = 1
57
+ cert.subject = OpenSSL::X509::Name.new([['CN', cn]])
58
+ cert.issuer = cert.subject
59
+ cert.public_key = key.public_key
60
+ cert.not_before = Time.now
61
+ cert.not_after = Time.now + (60 * 60 * 24 * 365)
62
+
63
+ cert.sign(key, OpenSSL::Digest::SHA256.new)
64
+
65
+ return cert
66
+ end
67
+ end
data/lib/koi.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ module Koi
3
+ VERSION = "1.0.0"
4
+ end
5
+
6
+ require_relative './koi/server'
7
+
8
+ module Koi
9
+ class Pond
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ def run
15
+ server = Koi::Server.new(@config)
16
+ server.run
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: koi-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Justin Woodring
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Koi is a ruby-based Gemini server that supports .erb templating, and static files, and handles TLS certs for you.
14
+
15
+ Koi is designed to make hosting Gemini CGI pages a joyful and
16
+ tranquil experience, uniting the conceptual beauty of a simple web
17
+ with developer happiness.
18
+
19
+ By using a combination of Ruby powered ERB templating and
20
+ traditional static file serving we distill a solution offering
21
+ the best of both worlds.
22
+ email: jwoodrg@gmail.com
23
+ executables:
24
+ - koi
25
+ extensions: []
26
+ extra_rdoc_files: []
27
+ files:
28
+ - ".gemspec"
29
+ - ".gitignore"
30
+ - Gemfile
31
+ - README.org
32
+ - bin/koi
33
+ - lib/koi.rb
34
+ - lib/koi/erb.rb
35
+ - lib/koi/erb/handler.rb
36
+ - lib/koi/file.rb
37
+ - lib/koi/gemini.rb
38
+ - lib/koi/gemini/request.rb
39
+ - lib/koi/gemini/response.rb
40
+ - lib/koi/gemini/status.rb
41
+ - lib/koi/server.rb
42
+ - lib/koi/ssl.rb
43
+ homepage: https://rubygems.org/gems/koi
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ source_code_uri: https://github.com/JustinWoodring/Koi
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 4.0.11
63
+ specification_version: 4
64
+ summary: Koi is a ruby-based Gemini server that supports ERB templating.
65
+ test_files: []