simple-httpd 0.0.4 → 0.3.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 +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
|