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.
- checksums.yaml +7 -0
- data/.gitignore +108 -0
- data/.gitmodules +9 -0
- data/.qb-options.yml +4 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/.yardopts +7 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +72 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/config/default.yml +24 -0
- data/doc/files/design/domains_and_labels.md +117 -0
- data/doc/files/topics/agents.md +16 -0
- data/doc/files/topics/launchd.md +15 -0
- data/doc/files/topics/plists.md +39 -0
- data/doc/include/plist.md +3 -0
- data/exe/locd +24 -0
- data/lib/locd.rb +75 -0
- data/lib/locd/agent.rb +1186 -0
- data/lib/locd/agent/job.rb +142 -0
- data/lib/locd/agent/proxy.rb +111 -0
- data/lib/locd/agent/rotate_logs.rb +82 -0
- data/lib/locd/agent/site.rb +174 -0
- data/lib/locd/agent/system.rb +270 -0
- data/lib/locd/cli.rb +4 -0
- data/lib/locd/cli/command.rb +9 -0
- data/lib/locd/cli/command/agent.rb +310 -0
- data/lib/locd/cli/command/base.rb +243 -0
- data/lib/locd/cli/command/job.rb +110 -0
- data/lib/locd/cli/command/main.rb +201 -0
- data/lib/locd/cli/command/proxy.rb +177 -0
- data/lib/locd/cli/command/rotate_logs.rb +152 -0
- data/lib/locd/cli/command/site.rb +47 -0
- data/lib/locd/cli/table.rb +157 -0
- data/lib/locd/config.rb +237 -0
- data/lib/locd/config/base.rb +93 -0
- data/lib/locd/errors.rb +65 -0
- data/lib/locd/label.rb +61 -0
- data/lib/locd/launchctl.rb +209 -0
- data/lib/locd/logging.rb +360 -0
- data/lib/locd/newsyslog.rb +402 -0
- data/lib/locd/pattern.rb +193 -0
- data/lib/locd/proxy.rb +272 -0
- data/lib/locd/proxymachine.rb +34 -0
- data/lib/locd/util.rb +49 -0
- data/lib/locd/version.rb +26 -0
- data/locd.gemspec +66 -0
- 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
|
data/lib/locd/version.rb
ADDED
@@ -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
|