locd 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|