spyder 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: acf6ba3240fd34ee197d51298611eaf540b096593d135899c6776cd7378f2726
4
- data.tar.gz: 0035fda272f0b56c184529166654daec8d3f8b0b6757bfda5e0cbc86d4010234
3
+ metadata.gz: d8fbffd16e4740ebf51ca1e51cb49c5a2f665408580340786ac36ba484f045c4
4
+ data.tar.gz: 6a0c26ce6168805cf1e1636835bd3aa4e376b022be5d4f2874bdfbc80682523e
5
5
  SHA512:
6
- metadata.gz: 033d5355015791027499d3650cfdf94e0d3dd4db326a4cd7b461b939f73744ae461b57f5213790dd5874edbfe8bddef25cc4abf0b866e474e28a7bdb47943b19
7
- data.tar.gz: 979996a1e7761986950816d84ca25b6dead8b5450736b1c2efb2aeddc17b8357028a12f9ec293488a0175a67cdd6c1d0ebb9ead0a4ac5301e3b76e02589c2fe5
6
+ metadata.gz: 4cac8464742015ecba92eb7d0ff9ca0d3c7bde3ca14ea274200344de1b1fd92aa424aede419ceab17afe6e60693f54ed6077c27ab233459496e03a780026b9b2
7
+ data.tar.gz: 37b4244fc29821e8efa337d7ffd0cf6e490bf1e94f526933bfafa1dea08b87c30b5312bfd8c6bf8209794108fab82bb0fc9debf9f2c72cc758cc16d9152104fd
@@ -0,0 +1,17 @@
1
+ require 'spyder'
2
+
3
+ server = Spyder::Server.new('0.0.0.0', 8080)
4
+
5
+ server.router.add_route 'GET', '/hello-world' do |request, _|
6
+ which_world = request.query_params['world'] || 'Earth'
7
+
8
+ resp = Spyder::Response.new
9
+ resp.add_standard_headers
10
+ resp.set_header 'content-type', 'text/plain'
11
+ resp.body = "hello from #{which_world}!"
12
+
13
+ resp
14
+ end
15
+
16
+ puts "Now navigate to http://localhost:8080/?world=Mars"
17
+ server.start
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spyder
4
+ class BaseError < ::StandardError
5
+ end
6
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ module Spyder
3
+ class HeaderStore
4
+ class MissingHeaderError < ::Spyder::BaseError
5
+ end
6
+
7
+ # headers as they have been received. Untransformed and in order
8
+ attr_reader :ordered
9
+
10
+ # headers, indexed. All keys are lower case
11
+ attr_reader :dict
12
+
13
+ # either :request or :response
14
+ attr_accessor :kind
15
+
16
+ def initialize kind=nil
17
+ @kind = kind
18
+ @ordered = []
19
+ @dict = {}
20
+ end
21
+
22
+ def fetch key, default_value=nil
23
+ value = @dict.fetch(key.downcase, default_value)
24
+ return value unless value == nil
25
+ return yield(key) if block_given?
26
+ nil
27
+ end
28
+
29
+ def get! key
30
+ value = fetch key
31
+ raise MissingHeaderError.new(key) if value == nil
32
+ value
33
+ end
34
+
35
+ def add_header key, value
36
+ @ordered << [key, value]
37
+ @dict[key.downcase] = value
38
+ end
39
+
40
+ def set_header key, value
41
+ key_lower = key.downcase
42
+ if @kind == :response && key_lower == 'set-cookie'
43
+ add_header key_lower, value
44
+ else
45
+ @dict[key_lower] = value
46
+ oh_index = @ordered.find_index { |k, v| k.downcase == key_lower }
47
+ if oh_index
48
+ @ordered[oh_index][1] = value
49
+ else
50
+ @ordered << [key, value]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spyder
4
+ class Request
5
+ extend Forwardable
6
+
7
+ attr_accessor :verb
8
+ attr_accessor :path
9
+ attr_accessor :headers
10
+ attr_accessor :protocol, :remote_address
11
+ attr_accessor :io
12
+
13
+ def_delegators :@headers, :ordered, :dict, :add_header
14
+
15
+ def initialize
16
+ @headers = HeaderStore.new(:request)
17
+ end
18
+
19
+ def host_with_port
20
+ result = dict['host']&.split(':')
21
+ return nil if result == nil
22
+ result << 443 if result.length < 2
23
+ result
24
+ end
25
+
26
+ def host
27
+ h = host_with_port
28
+ return nil if h == nil
29
+ h[0]
30
+ end
31
+
32
+ def port
33
+ h = host_with_port
34
+ return nil if h == nil
35
+ h[1]
36
+ end
37
+
38
+ def path_info
39
+ separator = path.index '?'
40
+ return path if separator == nil
41
+ path[0...separator]
42
+ end
43
+
44
+ def query_string
45
+ separator = path.index '?'
46
+ return nil if separator == nil
47
+ path[separator+1..-1]
48
+ end
49
+
50
+ def query_params
51
+ return {} unless query_string
52
+ res = {}
53
+ query_string.split('&').each do |line|
54
+ name, value = line.split('=')
55
+ name = CGI.unescape(name)
56
+ value = CGI.unescape(value)
57
+
58
+ if name.end_with?('[]') && name.length > 2
59
+ name = name[..-3]
60
+ arr = res.fetch(name, [])
61
+ arr = (res[name] = [])
62
+ arr << value
63
+ else
64
+ res[name] = value
65
+ end
66
+ end
67
+
68
+ res
69
+ end
70
+
71
+ def read_full_body
72
+ len = dict['content-length']
73
+ len ? io.read(Integer(len)) : io.read # FIXME:
74
+ end
75
+
76
+ def has_body?
77
+ dict['transfer-encoding'] != nil || dict['content-length'] != nil
78
+ end
79
+
80
+ def verb_allows_body?
81
+ %w(POST PUT PATCH).include?(verb)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spyder
4
+ class Response
5
+ attr_reader :code
6
+ attr_accessor :body
7
+ attr_reader :headers
8
+ attr_writer :reason_sentence
9
+
10
+ CODE_TO_REASON_SENTENCE = {
11
+ 100 => 'Continue',
12
+ 101 => 'Switching Protocols',
13
+ 200 => 'OK',
14
+ 201 => 'Created',
15
+ 202 => 'Accepted',
16
+ 203 => 'Non-Authoritative Information',
17
+ 204 => 'No Content',
18
+ 205 => 'Reset Content',
19
+ 206 => 'Partial Content',
20
+ 300 => 'Multiple Choices',
21
+ 301 => 'Moved Permanently',
22
+ 302 => 'Found',
23
+ 303 => 'See Other',
24
+ 304 => 'Not Modified',
25
+ 305 => 'Use Proxy',
26
+ 307 => 'Temporary Redirect',
27
+ 400 => 'Bad Request',
28
+ 401 => 'Unauthorized',
29
+ 402 => 'Payment Required',
30
+ 403 => 'Forbidden',
31
+ 404 => 'Not Found',
32
+ 405 => 'Method Not Allowed',
33
+ 406 => 'Not Acceptable',
34
+ 407 => 'Proxy Authentication Required',
35
+ 408 => 'Request Timeout',
36
+ 409 => 'Conflict',
37
+ 410 => 'Gone',
38
+ 411 => 'Length Required',
39
+ 412 => 'Precondition Failed',
40
+ 413 => 'Payload Too Large',
41
+ 414 => 'URI Too Long',
42
+ 415 => 'Unsupported Media Type',
43
+ 416 => 'Range Not Satisfiable',
44
+ 417 => 'Expectation Failed',
45
+ 418 => 'I\'m a teapot',
46
+ 422 => 'Unprocessable Entity',
47
+ 426 => 'Upgrade Required',
48
+ 500 => 'Internal Server Error',
49
+ 501 => 'Not Implemented',
50
+ 502 => 'Bad Gateway',
51
+ 503 => 'Service Unavailable',
52
+ 504 => 'Gateway Timeout',
53
+ 505 => 'HTTP Version Not Supported',
54
+ }.freeze
55
+
56
+ SYMBOL_TO_CODE = CODE_TO_REASON_SENTENCE.to_a.to_h do |code, name|
57
+ [
58
+ name.downcase.tr(' ', '_').tr("'", '').to_sym,
59
+ code
60
+ ]
61
+ end.freeze
62
+
63
+ def initialize
64
+ self.code = :ok
65
+ @headers = HeaderStore.new(:response)
66
+ end
67
+
68
+ def add_standard_headers
69
+ set_header 'date', Time.now.httpdate
70
+ set_header 'server', "spyder/#{::Spyder::VERSION}"
71
+ end
72
+
73
+ def nocache!
74
+ set_header 'expires', Time.at(0).httpdate
75
+ set_header 'cache-control', 'private, no-store, no-cache'
76
+ end
77
+
78
+ def code=(value)
79
+ @code = value.is_a?(Symbol) ? SYMBOL_TO_CODE.fetch(value) : value
80
+ end
81
+
82
+ def set_header(key, value)
83
+ @headers.set_header(key, value)
84
+ end
85
+
86
+ def reason_sentence
87
+ @reason_sentence || CODE_TO_REASON_SENTENCE[@code]
88
+ end
89
+
90
+ def self.make_generic(code, payload=nil)
91
+ new.tap do |r|
92
+ code = SYMBOL_TO_CODE.fetch(code, code)
93
+ r.body = (payload || "#{code} #{CODE_TO_REASON_SENTENCE[code]}")
94
+ r.code = code
95
+ r.add_standard_headers
96
+ r.nocache!
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spyder
4
+ class Router
5
+ attr_accessor :fallback_route
6
+
7
+ def initialize
8
+ @routes = Hash.new { |k, v| k[v] = [] }
9
+ end
10
+
11
+ def add_route(verb, matcher, &handler)
12
+ matcher = Mustermann.new(matcher) if matcher.is_a?(String)
13
+
14
+ @routes[verb.to_s.upcase] << [matcher, handler]
15
+ end
16
+
17
+ def call(ctx, request)
18
+ only_path = request.path_info
19
+
20
+ handler, match_data = nil
21
+ @routes[request.verb].any? do |mt, h|
22
+ md = mt.match(only_path)
23
+ if md
24
+ handler = h
25
+ match_data = md
26
+ end
27
+ end
28
+
29
+ match_data ? handler.call(request, match_data) : Response.make_generic(:not_found)
30
+ end
31
+ end
32
+
33
+ class RouterApp
34
+ def initialize(router, _)
35
+ @router = router
36
+ end
37
+
38
+ def call(ctx, request)
39
+ @router.call(ctx, request)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spyder
4
+ class Server
5
+ attr_accessor :router
6
+
7
+ def initialize(bind, port, router: Router.new, max_threads: 4)
8
+ @server = TCPServer.new(bind, port)
9
+ @max_threads = max_threads
10
+ @middleware = []
11
+ @threads = []
12
+ @tp_sync = Mutex.new
13
+ @router = router
14
+ end
15
+
16
+ def add_middleware(callable, args)
17
+ @middleware << [callable, args]
18
+ end
19
+
20
+ def start
21
+ @server.listen(10)
22
+ loop do
23
+ client = @server.accept
24
+
25
+ app_thread = Thread.new do
26
+ error = nil
27
+ begin
28
+ process_new_client(client)
29
+ rescue Exception => e
30
+ error = e
31
+ end
32
+
33
+ if error
34
+ puts error.full_message
35
+
36
+ response = Response.make_generic :internal_server_error
37
+ dispatch_response(client, response)
38
+ end
39
+
40
+ client.close rescue nil
41
+ end
42
+
43
+ over_capacity = true
44
+ added_thread_to_list = false
45
+ while over_capacity
46
+ @tp_sync.synchronize do
47
+ unless added_thread_to_list
48
+ @threads << app_thread
49
+ added_thread_to_list = true
50
+ end
51
+ over_capacity = (@threads.length >= @max_threads)
52
+ # puts("#{@threads.length} of #{@max_threads}")
53
+
54
+ @threads.delete_if { |t| !t.alive? }
55
+ end
56
+
57
+ # puts("XXX OVER CAPACITY!") if over_capacity
58
+ sleep 0 if over_capacity
59
+ end
60
+ end
61
+ end
62
+
63
+ def process_request(request)
64
+ mids = @middleware + [[RouterApp, @router]]
65
+ app = nil
66
+ loop do
67
+ klass, args = mids.pop
68
+ break unless klass
69
+ app = klass.new(args, app)
70
+ end
71
+
72
+ app.call({}, request)
73
+ end
74
+
75
+ def process_new_client(socket)
76
+ verb, path, protocol = read_line(socket).split(' ')
77
+ request = Request.new
78
+ request.path = path
79
+ request.verb = verb
80
+ request.io = socket
81
+
82
+ loop do
83
+ line = read_line(socket)
84
+ break if line == ''
85
+ sep = line.index(':')
86
+ name = line[0...sep].downcase
87
+ value = line[(sep + 2)..]
88
+ request.add_header(name, value)
89
+ end
90
+
91
+ response = process_request(request)
92
+
93
+ dispatch_response(socket, response)
94
+ end
95
+
96
+ def dispatch_response(socket, response)
97
+ content_length = response.headers.dict['content-length']
98
+ if !content_length && response.body && response.body.is_a?(String)
99
+ content_length = response.body.length
100
+ end
101
+
102
+ socket.write("HTTP/1.1 #{response.code} #{response.reason_sentence.b}\r\n")
103
+ response.headers.ordered.each do |name, value|
104
+ socket.write("#{name.b}: #{value.b}\r\n")
105
+ end
106
+ socket.write("connection: close\r\n") # FIXME:
107
+ socket.write("content-length: #{content_length}\r\n") if content_length
108
+ socket.write("\r\n")
109
+
110
+ if response.body
111
+ Array(response.body).each do |part|
112
+ content = part.respond_to?(:call) ? part.call : part
113
+ socket.write(content.b)
114
+ end
115
+ end
116
+ end
117
+
118
+ def read_line(socket)
119
+ line_limit = 1024 * 16
120
+ buffer = String.new(capacity: 128)
121
+ almost = false
122
+ loop do
123
+ line_limit -= 1
124
+ return false unless line_limit > 0
125
+
126
+ c = socket.readchar
127
+ if !almost && c == "\r"
128
+ almost = true
129
+ elsif almost
130
+ return false unless c == "\n"
131
+ return buffer
132
+ else
133
+ buffer += c
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spyder
4
+ VERSION = '0.1.1'
5
+ end
6
+
data/lib/spyder.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ require 'mustermann'
3
+ require 'date'
4
+ require 'time'
5
+ require 'forwardable'
6
+ require 'socket'
7
+ require 'cgi'
8
+
9
+ require 'spyder/version'
10
+ require 'spyder/error'
11
+ require 'spyder/header_store'
12
+ require 'spyder/request'
13
+ require 'spyder/response'
14
+ require 'spyder/router'
15
+ require 'spyder/server'
metadata CHANGED
@@ -1,43 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spyder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
- - André Piske
8
- autorequire:
7
+ - André D. Piske
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-17 00:00:00.000000000 Z
11
+ date: 2024-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: thor
14
+ name: mustermann
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.20.3
20
- type: :development
19
+ version: '3.0'
20
+ type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.20.3
27
- description: Reserved for the future
28
- email: ''
29
- executables:
30
- - spyder
26
+ version: '3.0'
27
+ description: Spyder Web
28
+ email: andrepiske@gmail.com
29
+ executables: []
31
30
  extensions: []
32
31
  extra_rdoc_files: []
33
32
  files:
34
- - bin/spyder
35
- homepage: https://rubygems.org/gems/spyder
33
+ - lib/examples/simplest_server/server.rb
34
+ - lib/spyder.rb
35
+ - lib/spyder/error.rb
36
+ - lib/spyder/header_store.rb
37
+ - lib/spyder/request.rb
38
+ - lib/spyder/response.rb
39
+ - lib/spyder/router.rb
40
+ - lib/spyder/server.rb
41
+ - lib/spyder/version.rb
42
+ homepage: https://github.com/andrepiske/spyder
36
43
  licenses:
37
44
  - MIT
38
- metadata:
39
- source_code_uri: https://github.com/andrepiske/spydergem
40
- post_install_message:
45
+ metadata: {}
46
+ post_install_message:
41
47
  rdoc_options: []
42
48
  require_paths:
43
49
  - lib
@@ -52,8 +58,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
58
  - !ruby/object:Gem::Version
53
59
  version: '0'
54
60
  requirements: []
55
- rubygems_version: 3.0.6
56
- signing_key:
61
+ rubygems_version: 3.5.6
62
+ signing_key:
57
63
  specification_version: 4
58
- summary: Reserved for the future
64
+ summary: Spyder
59
65
  test_files: []
data/bin/spyder DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env ruby
2
- require "thor"
3
- puts("LOL, not using thor at all")