locd 0.1.3

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.
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