locd 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +108 -0
  3. data/.gitmodules +9 -0
  4. data/.qb-options.yml +4 -0
  5. data/.rspec +3 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +7 -0
  8. data/Gemfile +10 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +72 -0
  11. data/Rakefile +6 -0
  12. data/VERSION +1 -0
  13. data/config/default.yml +24 -0
  14. data/doc/files/design/domains_and_labels.md +117 -0
  15. data/doc/files/topics/agents.md +16 -0
  16. data/doc/files/topics/launchd.md +15 -0
  17. data/doc/files/topics/plists.md +39 -0
  18. data/doc/include/plist.md +3 -0
  19. data/exe/locd +24 -0
  20. data/lib/locd.rb +75 -0
  21. data/lib/locd/agent.rb +1186 -0
  22. data/lib/locd/agent/job.rb +142 -0
  23. data/lib/locd/agent/proxy.rb +111 -0
  24. data/lib/locd/agent/rotate_logs.rb +82 -0
  25. data/lib/locd/agent/site.rb +174 -0
  26. data/lib/locd/agent/system.rb +270 -0
  27. data/lib/locd/cli.rb +4 -0
  28. data/lib/locd/cli/command.rb +9 -0
  29. data/lib/locd/cli/command/agent.rb +310 -0
  30. data/lib/locd/cli/command/base.rb +243 -0
  31. data/lib/locd/cli/command/job.rb +110 -0
  32. data/lib/locd/cli/command/main.rb +201 -0
  33. data/lib/locd/cli/command/proxy.rb +177 -0
  34. data/lib/locd/cli/command/rotate_logs.rb +152 -0
  35. data/lib/locd/cli/command/site.rb +47 -0
  36. data/lib/locd/cli/table.rb +157 -0
  37. data/lib/locd/config.rb +237 -0
  38. data/lib/locd/config/base.rb +93 -0
  39. data/lib/locd/errors.rb +65 -0
  40. data/lib/locd/label.rb +61 -0
  41. data/lib/locd/launchctl.rb +209 -0
  42. data/lib/locd/logging.rb +360 -0
  43. data/lib/locd/newsyslog.rb +402 -0
  44. data/lib/locd/pattern.rb +193 -0
  45. data/lib/locd/proxy.rb +272 -0
  46. data/lib/locd/proxymachine.rb +34 -0
  47. data/lib/locd/util.rb +49 -0
  48. data/lib/locd/version.rb +26 -0
  49. data/locd.gemspec +66 -0
  50. metadata +262 -0
data/lib/locd/proxy.rb ADDED
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+ # encoding: UTF-8
3
+
4
+ # Refinements
5
+ # =======================================================================
6
+
7
+ using NRSER
8
+
9
+
10
+ # Definitions
11
+ # =======================================================================
12
+
13
+ # Stuff for running the proxy server, which does "vhost"-style routing of
14
+ # HTTP requests it receives to user-defined sites.
15
+ #
16
+ # It does this by matching the HTTP `Host` header against site labels.
17
+ #
18
+ # Built off [proxymachine][], which is itself built on [eventmachine][].
19
+ #
20
+ # [proxymachine]: https://rubygems.org/gems/proxymachine
21
+ # [eventmachine]: https://rubygems.org/gems/eventmachine
22
+ #
23
+ module Locd::Proxy
24
+
25
+ # Constants
26
+ # ======================================================================
27
+
28
+ # {Regexp} to match HTTP "Host" header line.
29
+ #
30
+ # @return [Regexp]
31
+ #
32
+ HOST_RE = /^Host\:\ /i
33
+
34
+
35
+ # Label for the Loc'd proxy agent itself
36
+ #
37
+ # @return [String]
38
+ #
39
+ ROTATE_LOGS_LABEL = 'com.nrser.locd.rotate-logs'
40
+
41
+
42
+ # Mixins
43
+ # ============================================================================
44
+
45
+ # Add {.logger} and {#logger} methods
46
+ include SemanticLogger::Loggable
47
+
48
+
49
+ # Module Methods
50
+ # ======================================================================
51
+
52
+ # See if the lines include complete HTTP headers.
53
+ #
54
+ # Looks for the `'\r\n\r\n'` string that separates the headers from the
55
+ # body.
56
+ #
57
+ # @param [String] data
58
+ # Data received so far from {ProxyMachine}.
59
+ #
60
+ # @return [Boolean]
61
+ # `true` if `data` contains complete headers.
62
+ #
63
+ def self.headers_received? data
64
+ data.include? "\r\n\r\n"
65
+ end
66
+
67
+
68
+ # Generate an HTTP text response string.
69
+ #
70
+ # @param [String] status
71
+ # The HTTP status header.
72
+ #
73
+ # @param [String] text
74
+ # Text response body.
75
+ #
76
+ # @return [String]
77
+ # Full HTTP response.
78
+ #
79
+ def self.http_response_for status, text
80
+ [
81
+ "HTTP/1.1 #{ status }",
82
+ "Content-Type: text/plain; charset=utf-8",
83
+ "Status: #{ status }",
84
+ "",
85
+ text
86
+ ].join( "\r\n" )
87
+ end
88
+
89
+
90
+ # Get the request host from HTTP header lines.
91
+ #
92
+ # @param [Array<String>] lines
93
+ # @return [String]
94
+ #
95
+ def self.extract_host lines
96
+ lines.
97
+ find { |line| line =~ HOST_RE }.
98
+ chomp.
99
+ split( ' ', 2 )[1]
100
+ end
101
+
102
+
103
+ # Get the request path from HTTP header lines.
104
+ #
105
+ # @param [Array<String>] lines
106
+ # @return [String]
107
+ #
108
+ def self.extract_path lines
109
+ lines[0].split( ' ' )[1]
110
+ end
111
+
112
+
113
+ # Route request based on data, see {ProxyMachine} docs for details.
114
+ #
115
+ # @todo
116
+ # This finds the agent using the host as a pattern, so it will match
117
+ # with unique partial label. I think in the case that the host is not
118
+ # the full label it should probably return a HTTP redirect to the full
119
+ # label so that the user URL is bookmark-abel, etc...?
120
+ #
121
+ # @param [String] data
122
+ # Data received so far.
123
+ #
124
+ # @return [Hash<Symbol, (Hash | Boolean)]
125
+ # Command for ProxyMachine.
126
+ #
127
+ def self.route data
128
+ lines = data.lines
129
+
130
+ logger.debug "Received data:\n#{ lines.pretty_inspect }"
131
+
132
+ unless headers_received? data
133
+ logger.debug "Have not yet received HTTP headers, waiting..."
134
+ logger.debug lines: lines
135
+ return {noop: true}
136
+ end
137
+
138
+ logger.debug "HTTP headers received, processing...\n#{ }"
139
+ logger.debug lines: lines
140
+
141
+ host = extract_host lines
142
+ logger.debug host: host
143
+
144
+ path = extract_path lines
145
+ logger.debug path: path
146
+
147
+ # Label is the domain without the port
148
+ label = if host.include? ':'
149
+ host.split( ':', 2 )[0]
150
+ else
151
+ host
152
+ end
153
+
154
+ site = find_and_start_site label
155
+ remote_host = "#{ Locd.config[:site, :bind] }:#{ site.port }"
156
+
157
+ pm_cmd = {remote: remote_host}
158
+ logger.debug "Routing to remote", cmd: pm_cmd
159
+
160
+ return pm_cmd
161
+
162
+ rescue Locd::RequestError => error
163
+ logger.error error
164
+ error.to_proxy_machine_cmd
165
+ rescue Exception => error
166
+ logger.error error
167
+ {close: http_response_for( '500 Server Error', error.message )}
168
+ end
169
+
170
+
171
+ # Range of ports to allocate to {Locd::Agent::Site} when one is not
172
+ # provided by the user.
173
+ #
174
+ # Start (inclusive) and end (exclusive) values come from `site.ports.start`
175
+ # and `site.ports.end` config values, which default to
176
+ #
177
+ # 55000...56000
178
+ #
179
+ # @return [Range<Fixnum, Fixnum>]
180
+ #
181
+ def self.port_range
182
+ Locd.config[:site, :ports, :start]...Locd.config[:site, :ports, :end]
183
+ end
184
+
185
+
186
+ # Find a port in {.port_range} that is not already used by a
187
+ # {Locd::Agent::Site} to give to a new site.
188
+ #
189
+ # @return [Fixnum]
190
+ # Port number.
191
+ #
192
+ # @raise
193
+ # If a port can not be found.
194
+ #
195
+ def self.allocate_port
196
+ allocated_ports = Locd::Agent::Site.ports
197
+
198
+ port = port_range.find { |port| ! allocated_ports.include? port }
199
+
200
+ if port.nil?
201
+ raise "Could not allocate port for #{ remote_key }"
202
+ end
203
+
204
+ port
205
+ end
206
+
207
+ # Um, find and start a {Locd::Agent::Site} from a pattern.
208
+ #
209
+ # @param pattern (see Locd::Agent.find_only!)
210
+ # @return [Locd::Agent::Site]
211
+ # @raise (see Locd::Agent.find_only!)
212
+ #
213
+ def self.find_and_start_site pattern
214
+ logger.debug "Finding and starting site...", pattern: pattern
215
+
216
+ site = Locd::Agent::Site.find_only! pattern
217
+
218
+ logger.debug "Found site!", site: site
219
+
220
+ if site.running?
221
+ logger.debug "Site is RUNNING"
222
+ else
223
+ logger.debug "Site STOPPED, starting..."
224
+ site.start
225
+ logger.debug "Site started."
226
+ end
227
+
228
+ site
229
+ end
230
+
231
+
232
+ # Get the proxy's port from it's `.plist` if it exists, otherwise from
233
+ # the config setting.
234
+ #
235
+ # @return [Fixnum]
236
+ # Port number.
237
+ #
238
+ # @raise [TypeError]
239
+ # If we can't find a suitable config setting when looking for one.
240
+ #
241
+ def self.port
242
+ if proxy = Locd::Agent::Proxy.get
243
+ proxy.port
244
+ else
245
+ Locd.config[:proxy, :port, type: t.pos_int]
246
+ end
247
+ end
248
+
249
+
250
+ # Run the proxy server.
251
+ #
252
+ # @param [String] bind
253
+ # Address to bind to.
254
+ #
255
+ # @param [Fixnum] port
256
+ # Port to listen on.
257
+ #
258
+ # @return [void]
259
+ # Not sure if/when this method ever returns.
260
+ #
261
+ def self.serve bind: config[:proxy, :bind],
262
+ port: config[:proxy, :port]
263
+ logger.info "Loc'd is starting ProxyMachine, hang on a sec...",
264
+ bind: bind,
265
+ port: port
266
+
267
+ require 'locd/proxymachine'
268
+ ProxyMachine.set_router method( :route )
269
+ ProxyMachine.run 'locd', bind, port
270
+ end # .serve
271
+
272
+ end # module Locd::Proxy
@@ -0,0 +1,34 @@
1
+ ##
2
+ # This file...
3
+ #
4
+ # 1. Requires {ProxyMachine} (because requiring it boots it up, causing ruckus
5
+ # when you don't want to run it, like during specs).
6
+ #
7
+ # 2. Monkey-patch {ProxyMachine#fast_shutdown} so I can change the sleep
8
+ # time... 10 seconds is forever to wait when developing, and I don't see
9
+ # a way to configure it without patching.
10
+ #
11
+ ##
12
+
13
+ require 'proxymachine'
14
+
15
+ class ProxyMachine
16
+ @@max_fast_shutdown_seconds = 1
17
+
18
+ def self.fast_shutdown(signal)
19
+ EM.stop_server($server) if $server
20
+ # Can't log in trap context (guess you used to be able to?) and this method
21
+ # is only entered from trap contexts, so comment these out to avoid the
22
+ # warning...
23
+ #
24
+ # LOGGER.info "Received #{signal} signal. No longer accepting new connections."
25
+ # LOGGER.info "Maximum time to wait for connections is #{@@max_fast_shutdown_seconds} seconds."
26
+ # LOGGER.info "Waiting for #{ProxyMachine.count} connections to finish."
27
+ $server = nil
28
+ EM.stop if ProxyMachine.count == 0
29
+ Thread.new do
30
+ sleep @@max_fast_shutdown_seconds
31
+ exit!
32
+ end
33
+ end
34
+ end
data/lib/locd/util.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requirements
4
+ # =======================================================================
5
+
6
+ # Stdlib
7
+ # -----------------------------------------------------------------------
8
+
9
+ # Deps
10
+ # -----------------------------------------------------------------------
11
+
12
+ # Project / Package
13
+ # -----------------------------------------------------------------------
14
+
15
+
16
+ # Refinements
17
+ # =======================================================================
18
+
19
+ using NRSER
20
+
21
+
22
+ # Definitions
23
+ # =======================================================================
24
+
25
+ module Locd
26
+
27
+ def self.resolve project_root, path = nil
28
+ return project_root if path.nil?
29
+
30
+ if path.start_with? '//'
31
+ project_root / path[2..-1]
32
+ elsif path.start_with? '/'
33
+ path.to_pn
34
+ else
35
+ project_root / path
36
+ end
37
+ end
38
+
39
+
40
+ # Just like {.resolve} but returns a {String} (instead of a {Pathname}).
41
+ #
42
+ # @param (see .resolve)
43
+ # @return [String]
44
+ #
45
+ def self.resolve_to_s *args
46
+ resolve( *args ).to_s
47
+ end
48
+
49
+ end # module Locd
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'pathname'
5
+
6
+ module Locd
7
+ # Absolute, expanded path to the gem's root directory.
8
+ #
9
+ # @return [Pathname]
10
+ #
11
+ ROOT = ( Pathname.new( __FILE__ ).dirname / '..' / '..' ).expand_path
12
+
13
+
14
+ # String version read from `//VERSION`
15
+ #
16
+ # @return [String]
17
+ #
18
+ VERSION = (ROOT / 'VERSION').read
19
+
20
+
21
+ # The gem name
22
+ #
23
+ # @return [String]
24
+ #
25
+ GEM_NAME = 'locd'
26
+ end
data/locd.gemspec ADDED
@@ -0,0 +1,66 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "locd/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = Locd::GEM_NAME
8
+ spec.version = Locd::VERSION
9
+ spec.authors = ["nrser"]
10
+ spec.email = ["neil@ztkae.com"]
11
+
12
+ spec.summary = %q{Get loc'd out!}
13
+ # spec.description = %q{TODO: Write a longer description or delete this line.}
14
+ spec.homepage = "https://github.com/nrser/locd"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features|dev)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.required_ruby_version = '>= 2.3.0'
25
+
26
+ # Dependencies
27
+ # ============================================================================
28
+
29
+ # Development Dependencies
30
+ # ----------------------------------------------------------------------------
31
+
32
+ spec.add_development_dependency "bundler", "~> 1.16"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.7"
35
+
36
+ # Nicer REPL experience
37
+ spec.add_development_dependency "pry", '~> 0.10.4'
38
+ # GitHub-Flavored Markdown (GFM) for use with `yard`
39
+ spec.add_development_dependency 'github-markup', '~> 1.6'
40
+ # Doc site generation with `yard`
41
+ spec.add_development_dependency 'yard', '~> 0.9.12'
42
+ # Provider for `commonmarker`, the new GFM lib
43
+ spec.add_development_dependency 'yard-commonmarker', '~> 0.3.0'
44
+
45
+
46
+ # Runtime Dependencies
47
+ # ----------------------------------------------------------------------------
48
+
49
+ # My guns
50
+ spec.add_dependency "nrser", '>= 0.2.0.pre.1'
51
+
52
+ # Used to process command templates from projects' `//dev/locd.yml` files
53
+ spec.add_dependency "cmds", ">= 0.2.8"
54
+
55
+ # Content-aware proxy in Ruby based on EventMachine
56
+ spec.add_dependency 'proxymachine', '~> 1.2'
57
+
58
+ # Manipulate the `.plist` XML files that `launchd` uses for service defs
59
+ spec.add_dependency 'plist', '~> 3.4'
60
+
61
+ # Atli, my fork of Thor for CLI interface
62
+ spec.add_dependency 'atli', '>= 0.1.2'
63
+
64
+ #
65
+ # spec.add_dependency 'terminal-table', '~> 1.8'
66
+ end