simple-httpd 0.0.4 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.rubocop.yml +9 -0
- data/.tm_properties +1 -0
- data/Gemfile +21 -1
- data/Makefile +9 -0
- data/README.md +87 -2
- data/Rakefile +5 -0
- data/VERSION +1 -1
- data/bin/simple-httpd +13 -0
- data/examples/README.md +41 -0
- data/examples/ex1/ex1_helpers.rb +5 -0
- data/examples/ex1/root.rb +11 -0
- data/examples/ex2/README.txt +1 -0
- data/examples/ex2/ex2_helpers.rb +5 -0
- data/examples/ex2/helpers.rb +15 -0
- data/examples/ex2/info.rb +4 -0
- data/examples/ex2/root.rb +3 -0
- data/examples/ex3/example_service.rb +13 -0
- data/examples/services/example_service.rb +25 -0
- data/examples/services/explicit_example_service.rb +18 -0
- data/examples/v2/api.js +1 -0
- data/examples/v2/jobs.rb +13 -0
- data/examples/v2/root.rb +3 -0
- data/examples/v2/v2_helpers.rb +5 -0
- data/lib/simple-service.rb +3 -0
- data/lib/simple/httpd.rb +99 -25
- data/lib/simple/httpd/base_controller.rb +2 -2
- data/lib/simple/httpd/base_controller/error_handling.rb +45 -17
- data/lib/simple/httpd/base_controller/json.rb +15 -8
- data/lib/simple/httpd/cli.rb +99 -0
- data/lib/simple/httpd/helpers.rb +54 -0
- data/lib/simple/httpd/mount_spec.rb +106 -0
- data/lib/simple/httpd/rack.rb +17 -0
- data/lib/simple/httpd/rack/dynamic_mount.rb +66 -0
- data/lib/simple/httpd/rack/merger.rb +28 -0
- data/lib/simple/httpd/rack/static_mount.rb +50 -0
- data/lib/simple/httpd/server.rb +69 -0
- data/lib/simple/httpd/service.rb +70 -0
- data/lib/simple/httpd/version.rb +1 -1
- data/lib/simple/service.rb +69 -0
- data/lib/simple/service/action.rb +78 -0
- data/lib/simple/service/context.rb +46 -0
- data/scripts/release +2 -0
- data/scripts/release.rb +91 -0
- data/simple-httpd.gemspec +9 -19
- data/spec/simple/httpd/base_controller/httpd_cors_spec.rb +15 -0
- data/spec/simple/httpd/base_controller/httpd_debug_spec.rb +11 -0
- data/spec/simple/httpd/base_controller/httpd_x_processing_copy.rb +15 -0
- data/spec/simple/httpd/base_spec.rb +16 -0
- data/spec/simple/httpd/dynamic_mounting_spec.rb +33 -0
- data/spec/simple/httpd/helpers_spec.rb +15 -0
- data/spec/simple/httpd/rspec_httpd_spec.rb +17 -0
- data/spec/simple/httpd/services/service_explicit_spec.rb +34 -0
- data/spec/simple/httpd/services/service_spec.rb +34 -0
- data/spec/simple/httpd/static_mounting_spec.rb +13 -0
- data/spec/spec_helper.rb +30 -6
- data/spec/support/004_simplecov.rb +3 -12
- metadata +61 -84
- data/lib/simple/httpd/app.rb +0 -84
- data/lib/simple/httpd/app/file_server.rb +0 -19
- data/spec/simple/httpd/version_spec.rb +0 -10
- data/tasks/release.rake +0 -104
@@ -12,8 +12,8 @@ class Simple::Httpd::BaseController < Sinatra::Base
|
|
12
12
|
# Rails development mode, but on the other hand it is much faster, and
|
13
13
|
# probably useful 90% of the time.
|
14
14
|
configure :development do
|
15
|
-
require "sinatra/reloader"
|
16
|
-
register Sinatra::Reloader
|
15
|
+
# require "sinatra/reloader"
|
16
|
+
# register Sinatra::Reloader
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -1,6 +1,10 @@
|
|
1
1
|
require_relative "./json"
|
2
2
|
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
|
+
|
3
5
|
class Simple::Httpd::BaseController
|
6
|
+
H = ::Simple::Httpd::Helpers
|
7
|
+
|
4
8
|
set :show_exceptions, false
|
5
9
|
set :dump_errors, false
|
6
10
|
set :raise_errors, false
|
@@ -64,27 +68,51 @@ class Simple::Httpd::BaseController
|
|
64
68
|
description: error_description(exc)
|
65
69
|
end
|
66
70
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
# title: "Not authorized",
|
73
|
-
# description: "You don't have necessary powers to access this page."
|
74
|
-
# end
|
71
|
+
error(::Simple::Service::ArgumentError) do |exc|
|
72
|
+
render_error exc, status: 422,
|
73
|
+
title: "Invalid input #{exc.message}",
|
74
|
+
description: exc.message
|
75
|
+
end
|
75
76
|
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
77
|
+
# -- not authorized.---------------------------------------------------------
|
78
|
+
|
79
|
+
class NotAuthorizedError < RuntimeError
|
80
|
+
end
|
81
|
+
|
82
|
+
error(NotAuthorizedError) do
|
83
|
+
render_error e, status: 403,
|
84
|
+
title: "Not authorized",
|
85
|
+
description: "You don't have necessary powers to access this page."
|
86
|
+
end
|
87
|
+
|
88
|
+
def not_authorized!(msg)
|
89
|
+
raise NotAuthorizedError, msg
|
90
|
+
end
|
91
|
+
|
92
|
+
# -- login required.---------------------------------------------------------
|
93
|
+
|
94
|
+
class LoginRequiredError < RuntimeError
|
95
|
+
end
|
96
|
+
|
97
|
+
def login_required!(msg)
|
98
|
+
raise LoginRequiredError, msg
|
99
|
+
end
|
100
|
+
|
101
|
+
error(LoginRequiredError) do
|
102
|
+
render_error e, status: 404,
|
103
|
+
title: "Not found",
|
104
|
+
description: "The server failed to recognize affiliation. Please provide a valid Session-Id."
|
105
|
+
end
|
106
|
+
|
107
|
+
# -- resource not found.-----------------------------------------------------
|
84
108
|
|
85
109
|
class NotFoundError < RuntimeError
|
86
110
|
end
|
87
111
|
|
112
|
+
def not_found!(msg)
|
113
|
+
raise NotFoundError, msg
|
114
|
+
end
|
115
|
+
|
88
116
|
error(NotFoundError) do |exc|
|
89
117
|
render_error exc, status: 404,
|
90
118
|
title: "Not found",
|
@@ -117,7 +145,7 @@ class Simple::Httpd::BaseController
|
|
117
145
|
private
|
118
146
|
|
119
147
|
def error_type(exc)
|
120
|
-
"error/#{exc.class.name
|
148
|
+
"error/#{H.underscore exc.class.name}".gsub(/\/error$/, "")
|
121
149
|
end
|
122
150
|
|
123
151
|
def error_description(exc)
|
@@ -29,18 +29,25 @@ class Simple::Httpd::BaseController
|
|
29
29
|
|
30
30
|
public
|
31
31
|
|
32
|
-
def
|
33
|
-
@
|
32
|
+
def parsed_body
|
33
|
+
return @parsed_body if defined? @parsed_body
|
34
|
+
|
35
|
+
@parsed_body = parse_body
|
36
|
+
rescue RuntimeError => e
|
37
|
+
raise ArgumentError, e.to_s
|
34
38
|
end
|
35
39
|
|
36
40
|
private
|
37
41
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
42
|
+
def parse_body
|
43
|
+
case request.media_type
|
44
|
+
when "application/json"
|
45
|
+
request.body.rewind
|
46
|
+
body = request.body.read
|
47
|
+
body == "" ? {} : JSON.parse(body)
|
48
|
+
else
|
49
|
+
# parses form data
|
50
|
+
request.POST
|
41
51
|
end
|
42
|
-
|
43
|
-
request.body.rewind
|
44
|
-
JSON.parse(request.body.read)
|
45
52
|
end
|
46
53
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
2
|
+
|
3
|
+
module Simple
|
4
|
+
class Httpd
|
5
|
+
class << self
|
6
|
+
attr_accessor :env
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Simple::Httpd::CLI
|
12
|
+
include Simple::CLI
|
13
|
+
|
14
|
+
# Runs a simple httpd server
|
15
|
+
#
|
16
|
+
# A mount_spec is either the location of a directory, which would then be mounted
|
17
|
+
# at the "/" HTTP location, or a directory followed by a colon and where to mount
|
18
|
+
# the directory.
|
19
|
+
#
|
20
|
+
# Mounted directories might contain either ruby source code which is then executed
|
21
|
+
# or static files to be delivered verbatim. See README.md for more details.
|
22
|
+
#
|
23
|
+
# Examples:
|
24
|
+
#
|
25
|
+
# simple-httpd --port=8080 httpd/root --service=src/to/service.rb MyService:/ httpd/assets:assets
|
26
|
+
#
|
27
|
+
# serves the content of ./httpd/root on http://0.0.0.0/ and the content of httpd/assets
|
28
|
+
# on http://0.0.0.0/assets.
|
29
|
+
#
|
30
|
+
# Options:
|
31
|
+
#
|
32
|
+
# --environment=ENV ... the environment setting, which adjusts configuration.
|
33
|
+
# --services=<path>,<path> ... load these ruby file during startup. Used to define service objects.
|
34
|
+
#
|
35
|
+
# simple-httpd respects the HOST and PORT environment values to determine the interface
|
36
|
+
# and port to listen to. Default values are "127.0.0.1" and 8181.
|
37
|
+
#
|
38
|
+
# Each entry in mounts can be either:
|
39
|
+
#
|
40
|
+
# - a mount_point <tt>[ mount_point, path ]</tt>, e.g. <tt>[ "path/to/root", "/"]</tt>
|
41
|
+
# - a string denoting a mount_point, e.g. "path/to/root:/")
|
42
|
+
# - a string denoting a "/" mount_point (e.g. "path", which is shorthand for "path:/")
|
43
|
+
def main(*mount_specs, environment: "development", services: nil)
|
44
|
+
::Simple::Httpd.env = environment
|
45
|
+
|
46
|
+
start_simplecov if environment == "test"
|
47
|
+
|
48
|
+
mount_specs << "." if mount_specs.empty?
|
49
|
+
|
50
|
+
host = ENV["HOST"] || "127.0.0.1"
|
51
|
+
port = Integer(ENV["PORT"] || 8181)
|
52
|
+
|
53
|
+
# late loading simple/httpd, for simplecov support
|
54
|
+
require "simple/httpd"
|
55
|
+
helpers = ::Simple::Httpd::Helpers
|
56
|
+
|
57
|
+
services&.split(",")&.each do |service_path|
|
58
|
+
paths = if Dir.exist?(service_path)
|
59
|
+
Dir.glob("#{service_path}/**/*.rb").sort
|
60
|
+
else
|
61
|
+
[service_path]
|
62
|
+
end
|
63
|
+
|
64
|
+
paths.each do |path|
|
65
|
+
logger.info "Loading service(s) from #{helpers.shorten_path path}"
|
66
|
+
load path
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
::Simple::Httpd.listen!(*mount_specs, environment: environment,
|
71
|
+
host: host,
|
72
|
+
port: port,
|
73
|
+
logger: logger)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def stderr_logger
|
79
|
+
logger = ::Logger.new STDERR
|
80
|
+
logger.level = ::Logger::INFO
|
81
|
+
logger
|
82
|
+
end
|
83
|
+
|
84
|
+
def start_simplecov
|
85
|
+
require "simplecov"
|
86
|
+
|
87
|
+
SimpleCov.command_name "Integration Tests"
|
88
|
+
SimpleCov.start do
|
89
|
+
# return true to remove src from coverage
|
90
|
+
add_filter do |src|
|
91
|
+
next true if src.filename =~ /\/spec\//
|
92
|
+
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
# minimum_coverage 90
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Simple::Httpd::Helpers
|
2
|
+
extend self
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def pwd
|
7
|
+
@pwd ||= File.join(Dir.getwd, "/")
|
8
|
+
end
|
9
|
+
|
10
|
+
def home
|
11
|
+
@home ||= File.join(Dir.home, "/")
|
12
|
+
end
|
13
|
+
|
14
|
+
public
|
15
|
+
|
16
|
+
def shorten_path(path)
|
17
|
+
path = File.absolute_path(path)
|
18
|
+
|
19
|
+
if path.start_with?(pwd)
|
20
|
+
path = path[pwd.length..-1]
|
21
|
+
path = File.join("./", path) if path =~ /\//
|
22
|
+
end
|
23
|
+
|
24
|
+
if path.start_with?(home)
|
25
|
+
path = File.join("~/", path[home.length..-1])
|
26
|
+
end
|
27
|
+
|
28
|
+
path
|
29
|
+
end
|
30
|
+
|
31
|
+
def underscore(str)
|
32
|
+
parts = str.split("::")
|
33
|
+
parts = parts.map do |part|
|
34
|
+
part.gsub(/[A-Z]+/) { |ch| "_#{ch.downcase}" }.gsub(/^_/, "")
|
35
|
+
end
|
36
|
+
parts.join("/")
|
37
|
+
end
|
38
|
+
|
39
|
+
# instance_eval zero or more paths in the context of obj
|
40
|
+
def instance_eval_paths(obj, *paths)
|
41
|
+
paths.each do |path|
|
42
|
+
# STDERR.puts "Loading #{path}"
|
43
|
+
obj.instance_eval File.read(path), path, 1
|
44
|
+
end
|
45
|
+
obj
|
46
|
+
end
|
47
|
+
|
48
|
+
# subclass a klass with an optional description
|
49
|
+
def subclass(klass, description: nil)
|
50
|
+
subclass = Class.new(klass)
|
51
|
+
subclass.define_method(:inspect) { description } if description
|
52
|
+
subclass
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# rubocop:disable Metrics/AbcSize, Style/ParallelAssignment
|
2
|
+
|
3
|
+
require "simple-service"
|
4
|
+
require_relative "./rack"
|
5
|
+
|
6
|
+
class Simple::Httpd::MountSpec
|
7
|
+
def self.build(arg, at:)
|
8
|
+
if at
|
9
|
+
entity, mount_point = arg, at
|
10
|
+
else
|
11
|
+
# The regexp below uses negative lookbehind and negative lookahead to
|
12
|
+
# only match single colons, but not double (or more) colons. See
|
13
|
+
# `ri Regexp` for details.
|
14
|
+
entity, mount_point = *arg.split(/(?<!:):(?!:)/, 2)
|
15
|
+
end
|
16
|
+
|
17
|
+
mount_point = normalize_and_verify_mount_point(mount_point)
|
18
|
+
|
19
|
+
ServiceMountSpec.build(mount_point, service: entity) ||
|
20
|
+
PathMountSpec.build(mount_point, path: entity) ||
|
21
|
+
raise(ArgumentError, "#{mount_point}: don't know how to mount #{entity.inspect}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.normalize_and_verify_mount_point(mount_point)
|
25
|
+
mount_point ||= "/" # fall back to "/"
|
26
|
+
mount_point = File.join("/", mount_point) # make sure we start at "/"
|
27
|
+
|
28
|
+
canary_url = "http://0.0.0.0#{mount_point}" # verify mount_point: can it be used to build a URL?
|
29
|
+
URI.parse canary_url
|
30
|
+
|
31
|
+
mount_point
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :mount_point
|
35
|
+
|
36
|
+
class PathMountSpec < ::Simple::Httpd::MountSpec
|
37
|
+
Rack = ::Simple::Httpd::Rack
|
38
|
+
|
39
|
+
attr_reader :path, :mount_point
|
40
|
+
|
41
|
+
def self.build(mount_point, path:)
|
42
|
+
path = path.gsub(/\/$/, "") # remove trailing "/"
|
43
|
+
|
44
|
+
raise ArgumentError, "You probably don't want to mount your root directory, check mount_spec" if path == ""
|
45
|
+
return unless Dir.exist?(path)
|
46
|
+
|
47
|
+
new(mount_point, path)
|
48
|
+
end
|
49
|
+
|
50
|
+
def initialize(mount_point, path)
|
51
|
+
@mount_point, @path = mount_point, path
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_rack_apps
|
55
|
+
[
|
56
|
+
Rack::DynamicMount.build(mount_point, path),
|
57
|
+
Rack::StaticMount.build(mount_point, path)
|
58
|
+
].compact
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class ServiceMountSpec < ::Simple::Httpd::MountSpec
|
63
|
+
H = ::Simple::Httpd::Helpers
|
64
|
+
|
65
|
+
attr_reader :service
|
66
|
+
|
67
|
+
def self.build(mount_point, service:)
|
68
|
+
service = ::Simple::Service.resolve(service)
|
69
|
+
return unless service
|
70
|
+
|
71
|
+
new(mount_point, service)
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize(mount_point, service)
|
75
|
+
@mount_point, @service = mount_point, service
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_rack_apps
|
79
|
+
[build_controller]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# wraps all helpers into a Simple::Httpd::BaseController subclass
|
85
|
+
def build_controller
|
86
|
+
controller = H.subclass(::Simple::Httpd::BaseController)
|
87
|
+
setup_action_routes! controller
|
88
|
+
controller
|
89
|
+
end
|
90
|
+
|
91
|
+
def setup_action_routes!(controller)
|
92
|
+
action_names = service.actions.keys
|
93
|
+
|
94
|
+
controller.mount_service(service) do |service|
|
95
|
+
action_names.each do |action_name|
|
96
|
+
::Simple::Httpd.logger.debug "#{mount_point}/#{action_name} -> #{service.name}##{action_name}"
|
97
|
+
controller.post "/#{action_name}" => action_name
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
::Simple::Httpd.logger.info do
|
102
|
+
"#{mount_point}: mounting #{action_names.count} actions(s) from #{service.name} service"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Simple::Httpd::Rack
|
2
|
+
end
|
3
|
+
|
4
|
+
require_relative "rack/static_mount"
|
5
|
+
require_relative "rack/dynamic_mount"
|
6
|
+
require_relative "rack/merger"
|
7
|
+
|
8
|
+
module Simple::Httpd::Rack
|
9
|
+
def self.merge(apps)
|
10
|
+
Merger.build(apps)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.error(status, message = nil)
|
14
|
+
message ||= "Error #{status}"
|
15
|
+
[status, {}, [message]]
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "expectation"
|
2
|
+
|
3
|
+
# The Simple::Httpd::Mountpoint.build returns a Rack compatible app, which
|
4
|
+
# serves HTTP requests according to a set of dynamic ruby scripts and some
|
5
|
+
# existing static files.
|
6
|
+
class Simple::Httpd::Rack::DynamicMount
|
7
|
+
H = ::Simple::Httpd::Helpers
|
8
|
+
Rack = ::Simple::Httpd::Rack
|
9
|
+
|
10
|
+
def self.build(mount_point, path)
|
11
|
+
expect! path => String
|
12
|
+
|
13
|
+
url_map = new(mount_point, path).build_url_map
|
14
|
+
|
15
|
+
::Rack::URLMap.new(url_map)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :path
|
19
|
+
attr_reader :mount_point
|
20
|
+
|
21
|
+
def initialize(mount_point, path)
|
22
|
+
@mount_point = mount_point
|
23
|
+
@path = path
|
24
|
+
|
25
|
+
# determine source_paths and controller_paths
|
26
|
+
source_paths = Dir.glob("#{path}/**/*.rb")
|
27
|
+
helper_paths, @controller_paths = source_paths.partition { |str| /_helpers\.rb$/ =~ str }
|
28
|
+
|
29
|
+
# build root_controller
|
30
|
+
@root_controller = build_root_controller(helper_paths)
|
31
|
+
end
|
32
|
+
|
33
|
+
# rubocop:disable Metrics/AbcSize
|
34
|
+
def build_url_map
|
35
|
+
@controller_paths.sort.each_with_object({}) do |absolute_path, url_map|
|
36
|
+
relative_path = absolute_path[(path.length)..-1]
|
37
|
+
|
38
|
+
relative_mount_point = relative_path == "/root.rb" ? "/" : relative_path.gsub(/\.rb$/, "")
|
39
|
+
controller_class = load_controller absolute_path
|
40
|
+
|
41
|
+
::Simple::Httpd.logger.info do
|
42
|
+
absolute_mount_point = File.join(mount_point, relative_mount_point)
|
43
|
+
routes_count = controller_class.routes.reject { |verb, _| verb == "HEAD" }.values.sum(&:count)
|
44
|
+
|
45
|
+
"#{absolute_mount_point}: mounting #{routes_count} route(s) from #{H.shorten_path absolute_path}"
|
46
|
+
end
|
47
|
+
|
48
|
+
url_map.update relative_mount_point => controller_class
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# wraps all helpers into a Simple::Httpd::BaseController subclass
|
53
|
+
def build_root_controller(helper_paths)
|
54
|
+
klass = H.subclass ::Simple::Httpd::BaseController,
|
55
|
+
description: "root controller at #{path} w/#{helper_paths.count} helpers"
|
56
|
+
|
57
|
+
H.instance_eval_paths klass, *helper_paths.sort
|
58
|
+
klass
|
59
|
+
end
|
60
|
+
|
61
|
+
# wraps the source file in path into a root_controller
|
62
|
+
def load_controller(path)
|
63
|
+
klass = H.subclass @root_controller, description: "controller at #{path}"
|
64
|
+
H.instance_eval_paths klass, path
|
65
|
+
end
|
66
|
+
end
|