conjur-asset-ui-api 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.git-hooks/pre_commit/ensure_livescript_compiled.rb +31 -0
- data/.git-hooks/pre_commit/trailing_whitespace.rb +26 -0
- data/.gitignore +20 -0
- data/.overcommit.yml +5 -0
- data/.project +18 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/compile_ls +2 -0
- data/conjur-asset-ui.gemspec +36 -0
- data/lib/conjur-asset-ui-version.rb +7 -0
- data/lib/conjur-asset-ui.rb +7 -0
- data/lib/conjur/audit/follower.rb +63 -0
- data/lib/conjur/audit/humanizer.rb +53 -0
- data/lib/conjur/audit/tableizer.rb +55 -0
- data/lib/conjur/command/ui.rb +38 -0
- data/lib/conjur/webserver/api_proxy.rb +94 -0
- data/lib/conjur/webserver/audit_stream.rb +92 -0
- data/lib/conjur/webserver/authorize.rb +28 -0
- data/lib/conjur/webserver/conjur_info.rb +33 -0
- data/lib/conjur/webserver/home.rb +36 -0
- data/lib/conjur/webserver/login.rb +50 -0
- data/lib/conjur/webserver/server.rb +111 -0
- data/livescript/views/audit.ls +124 -0
- data/public/css/bootstrap.css +7 -0
- data/public/css/styles.less +400 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/images/conjur-logo.svg +26 -0
- data/public/images/icon-client-pc.svg +12 -0
- data/public/images/icon-environment.png +0 -0
- data/public/images/icon-person.svg +12 -0
- data/public/images/icon-service-dots.svg +13 -0
- data/public/images/icon-variable.png +0 -0
- data/public/index.html +121 -0
- data/public/js/lib/JSXTransformer.js +10862 -0
- data/public/js/lib/async.js +958 -0
- data/public/js/lib/backbone.js +2 -0
- data/public/js/lib/bootstrap.js +6 -0
- data/public/js/lib/date.extensions.js +141 -0
- data/public/js/lib/less.js +16 -0
- data/public/js/lib/moment.js +7768 -0
- data/public/js/lib/pace.js +2 -0
- data/public/js/lib/prelude-browser-min.js +1 -0
- data/public/js/lib/react-with-addons.js +15505 -0
- data/public/js/lib/react.js +14469 -0
- data/public/js/lib/sorted-set.no-require.js +1170 -0
- data/public/js/lib/sorted-set.no-require.js.txt +6 -0
- data/public/js/lib/underscore-min.js +6 -0
- data/public/js/lib/underscore.string.min.js +1 -0
- data/public/js/main.js +353 -0
- data/public/js/models/namespace.js +6 -0
- data/public/js/models/policyList.js +10 -0
- data/public/js/models/record.js +26 -0
- data/public/js/models/resourceList.js +61 -0
- data/public/js/models/userList.js +16 -0
- data/public/js/models/variableList.js +12 -0
- data/public/js/views/audit.js +191 -0
- data/public/js/views/dashboard.js +35 -0
- data/public/js/views/generic.js +42 -0
- data/public/js/views/group.js +32 -0
- data/public/js/views/groups.js +18 -0
- data/public/js/views/host.js +40 -0
- data/public/js/views/hosts.js +18 -0
- data/public/js/views/layer.js +63 -0
- data/public/js/views/layers.js +18 -0
- data/public/js/views/mixins/search.js +9 -0
- data/public/js/views/namespaces.js +40 -0
- data/public/js/views/navSearch.js +16 -0
- data/public/js/views/permissions.js +91 -0
- data/public/js/views/policies.js +17 -0
- data/public/js/views/policy.js +23 -0
- data/public/js/views/resource.js +23 -0
- data/public/js/views/role.js +18 -0
- data/public/js/views/searchResults.js +146 -0
- data/public/js/views/time.js +14 -0
- data/public/js/views/user.js +22 -0
- data/public/js/views/users.js +18 -0
- data/public/js/views/variable.js +41 -0
- data/public/js/views/variables.js +18 -0
- data/vendor/prelude-ls/.gitignore +2 -0
- data/vendor/prelude-ls/.travis.yml +3 -0
- data/vendor/prelude-ls/CHANGELOG.md +81 -0
- data/vendor/prelude-ls/LICENSE +22 -0
- data/vendor/prelude-ls/Makefile +50 -0
- data/vendor/prelude-ls/README.md +15 -0
- data/vendor/prelude-ls/browser/prelude-browser-min.js +1 -0
- data/vendor/prelude-ls/browser/prelude-browser.js +1172 -0
- data/vendor/prelude-ls/lib/Func.js +40 -0
- data/vendor/prelude-ls/lib/List.js +602 -0
- data/vendor/prelude-ls/lib/Num.js +129 -0
- data/vendor/prelude-ls/lib/Obj.js +153 -0
- data/vendor/prelude-ls/lib/Str.js +68 -0
- data/vendor/prelude-ls/lib/index.js +164 -0
- data/vendor/prelude-ls/package.json +50 -0
- data/vendor/prelude-ls/package.ls +46 -0
- data/vendor/prelude-ls/src/Func.ls +17 -0
- data/vendor/prelude-ls/src/List.ls +299 -0
- data/vendor/prelude-ls/src/Num.ls +83 -0
- data/vendor/prelude-ls/src/Obj.ls +61 -0
- data/vendor/prelude-ls/src/Str.ls +32 -0
- data/vendor/prelude-ls/src/index.ls +56 -0
- data/vendor/prelude-ls/test/Func.ls +36 -0
- data/vendor/prelude-ls/test/List.ls +751 -0
- data/vendor/prelude-ls/test/Num.ls +258 -0
- data/vendor/prelude-ls/test/Obj.ls +145 -0
- data/vendor/prelude-ls/test/Prelude.ls +49 -0
- data/vendor/prelude-ls/test/Str.ls +208 -0
- data/vendor/prelude-ls/test/browser.html +5 -0
- metadata +328 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
module Conjur
|
2
|
+
module WebServer
|
3
|
+
require 'rack/streaming_proxy'
|
4
|
+
|
5
|
+
class APIProxy < Rack::StreamingProxy::Proxy
|
6
|
+
|
7
|
+
class Request < Rack::StreamingProxy::Request
|
8
|
+
def initialize env
|
9
|
+
path = env["PATH_INFO"]
|
10
|
+
|
11
|
+
path =~ /^\/([^\/]+)(.*)/
|
12
|
+
app = $1
|
13
|
+
path_remainder = $2
|
14
|
+
|
15
|
+
new_url = case app
|
16
|
+
when 'authn', 'authz', 'audit', 'pubkeys'
|
17
|
+
[ Conjur.configuration.send("#{app}_url"), path_remainder ].join
|
18
|
+
else
|
19
|
+
[ Conjur.configuration.send("core_url"), path ].join
|
20
|
+
end
|
21
|
+
if query = env["QUERY_STRING"]
|
22
|
+
new_url = [ new_url, query ].join('?')
|
23
|
+
end
|
24
|
+
|
25
|
+
super new_url.to_s, Rack::Request.new(env)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Rack::StreamingProxy::Proxy.set_default_configuration
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
super nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def call env
|
36
|
+
request = Request.new(env)
|
37
|
+
request.http_request['Authorization'] = authorization_header
|
38
|
+
response = Rack::StreamingProxy::Session.new(request).start
|
39
|
+
rewrite_response(env, response.status, response.headers, response)
|
40
|
+
end
|
41
|
+
|
42
|
+
def rewrite_response(*args)
|
43
|
+
env, status, headers, body = args
|
44
|
+
|
45
|
+
source_request = Rack::Request.new(env)
|
46
|
+
|
47
|
+
headers = Hash[*headers.flat_map { |k, v| [capitalize_header(k), v] }]
|
48
|
+
headers.delete 'Transfer-Encoding' # let Puma handle chunking
|
49
|
+
|
50
|
+
# Rewrite location
|
51
|
+
if location = headers["Location"]
|
52
|
+
headers["Location"] = location.gsub(Conjur.configuration.service_url, "http://#{source_request.host}:#{source_request.port}")
|
53
|
+
end
|
54
|
+
|
55
|
+
[ status, headers, body ]
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def authorization_header
|
61
|
+
require 'conjur/authn'
|
62
|
+
require 'base64'
|
63
|
+
token = Conjur::Authn.authenticate
|
64
|
+
"Token token=\"#{Base64.strict_encode64(token.to_json)}\""
|
65
|
+
end
|
66
|
+
|
67
|
+
def perform_request(env)
|
68
|
+
triplet = super(env)
|
69
|
+
[ env ] + triplet
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def capitalize_header hdr
|
75
|
+
hdr.split('-').map(&:capitalize).join('-')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Rack::StreamingProxy by default doesn't handle closing, which leads to stale
|
82
|
+
# (but still running) connections. Handle the close by quitting the child
|
83
|
+
# (it will close connection with upstream automatically).
|
84
|
+
class Rack::StreamingProxy::Response
|
85
|
+
def initialize piper
|
86
|
+
@piper = piper
|
87
|
+
@client_http_version = '1.0'
|
88
|
+
receive
|
89
|
+
end
|
90
|
+
|
91
|
+
def close
|
92
|
+
@piper.signal :QUIT
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'conjur/audit/humanizer'
|
3
|
+
require 'conjur/audit/tableizer'
|
4
|
+
|
5
|
+
module Conjur
|
6
|
+
module WebServer
|
7
|
+
class AuditStream
|
8
|
+
include Conjur::Audit::Humanizer
|
9
|
+
include Conjur::Audit::Tableizer
|
10
|
+
|
11
|
+
class Body
|
12
|
+
include EM::Deferrable
|
13
|
+
def write chunk
|
14
|
+
@callable.call chunk
|
15
|
+
end
|
16
|
+
def each &block
|
17
|
+
@callable = block
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
HEADERS = {
|
22
|
+
"Content-Type" => "text/event-stream",
|
23
|
+
"Connection" => "keepalive",
|
24
|
+
"Cache-Control" => "no-cache, no-store"
|
25
|
+
}
|
26
|
+
|
27
|
+
def call env
|
28
|
+
body = Body.new
|
29
|
+
stream_events(env) do |events|
|
30
|
+
write_events body, events
|
31
|
+
end
|
32
|
+
[200, HEADERS, body]
|
33
|
+
end
|
34
|
+
|
35
|
+
def stream_events env, &block
|
36
|
+
# This could be a lot more "EventMachineish" by using for example
|
37
|
+
# EM::HttpRequest, but putting it in the thread pool should be
|
38
|
+
# good enough for our purposes.
|
39
|
+
EM.defer do
|
40
|
+
follower = Conjur::Audit::Follower.new{|opts| fetch_events(env, opts)}
|
41
|
+
follower.filter{|e| self_event?(env, e)} unless show_self_events?(env)
|
42
|
+
follower.follow &block
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if this looks like a permission check performed by the
|
47
|
+
# audit service
|
48
|
+
def self_event? env, e
|
49
|
+
e['action'] == 'check' && e['asset'] == 'resource' && e['conjur_role'] == e['role'] && e['role'] == env['conjur.roleid']
|
50
|
+
end
|
51
|
+
|
52
|
+
def show_self_events? env
|
53
|
+
!!Rack::Request.new(env).params['self']
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns [kind, id]
|
57
|
+
def parse_path env
|
58
|
+
path = env["SCRIPT_NAME"] + env["PATH_INFO"]
|
59
|
+
%r{^/api/audit/stream/(.*?)(?:/(.*))?$} =~ path
|
60
|
+
[$1, $2]
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_events env, options
|
64
|
+
kind, id = parse_path env
|
65
|
+
args = if kind == 'role' && id.nil?
|
66
|
+
[:audit_current_role, options]
|
67
|
+
else
|
68
|
+
[:"audit_#{kind}", id, options]
|
69
|
+
end
|
70
|
+
format = Rack::Request.new(env).params['format'] || 'string'
|
71
|
+
format_method = case format
|
72
|
+
when 'table'
|
73
|
+
:tableize
|
74
|
+
else
|
75
|
+
:humanize
|
76
|
+
end
|
77
|
+
api.send(*args).each {|e| send(format_method, e)}
|
78
|
+
end
|
79
|
+
|
80
|
+
def write_events body, events
|
81
|
+
events.each do |e|
|
82
|
+
body.write "id: #{e['event_id']}\n"
|
83
|
+
body.write "data: #{JSON.generate e}\n\n"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def api
|
88
|
+
Conjur::API.new_from_token Conjur::Authn.authenticate
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Conjur
|
2
|
+
module WebServer
|
3
|
+
# Verifies that the request contains the authorization token, and then strips it.
|
4
|
+
class Authorize
|
5
|
+
attr_reader :app, :sessionid
|
6
|
+
|
7
|
+
def initialize(app, sessionid)
|
8
|
+
@app = app
|
9
|
+
@sessionid = sessionid
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
if token_valid?(env)
|
14
|
+
@app.call env
|
15
|
+
else
|
16
|
+
[403, {}, ["Authorization is missing or invalid"]]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def token_valid?(env)
|
23
|
+
request = Rack::Request.new(env)
|
24
|
+
request.session[:sessionid] == sessionid
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Conjur
|
2
|
+
module WebServer
|
3
|
+
# Middleware that adds some conjur info to the rack environment
|
4
|
+
class ConjurInfo
|
5
|
+
def initialize app
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call env
|
10
|
+
update_env env
|
11
|
+
@app.call env
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_env env
|
15
|
+
PROPERTIES.each{|name| env["conjur.#{name}"] = send(name)}
|
16
|
+
end
|
17
|
+
|
18
|
+
PROPERTIES = %w(roleid account stack)
|
19
|
+
|
20
|
+
def roleid
|
21
|
+
"#{account}:user:#{Conjur::Authn.get_credentials[0]}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def account
|
25
|
+
Conjur.account
|
26
|
+
end
|
27
|
+
|
28
|
+
def stack
|
29
|
+
Conjur.stack
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'rack/utils'
|
3
|
+
require 'rack/mime'
|
4
|
+
|
5
|
+
module Conjur
|
6
|
+
module WebServer
|
7
|
+
class Home
|
8
|
+
F = ::File
|
9
|
+
|
10
|
+
def initialize(root)
|
11
|
+
@root = root
|
12
|
+
end
|
13
|
+
|
14
|
+
# From Rack::File
|
15
|
+
def call(env)
|
16
|
+
path = File.expand_path("index.html", @root)
|
17
|
+
|
18
|
+
if env["REQUEST_METHOD"] == "OPTIONS"
|
19
|
+
return [200, {'Allow' => ALLOW_HEADER, 'Content-Length' => '0'}, []]
|
20
|
+
end
|
21
|
+
last_modified = F.mtime(path).httpdate
|
22
|
+
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
|
23
|
+
|
24
|
+
size = F.size?(path) || Rack::Utils.bytesize(F.read(path))
|
25
|
+
|
26
|
+
headers = {
|
27
|
+
"Last-Modified" => last_modified,
|
28
|
+
"Content-Type" => "text/html",
|
29
|
+
"Content-Length" => size.to_s
|
30
|
+
}
|
31
|
+
|
32
|
+
[ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : [ F.read(path) ] ]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
module Conjur
|
3
|
+
module WebServer
|
4
|
+
class Login
|
5
|
+
attr_reader :sessionid
|
6
|
+
|
7
|
+
def initialize(sessionid)
|
8
|
+
@sessionid = sessionid
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
if sessionid = token_valid?(env)
|
13
|
+
env["rack.session"][:sessionid] = sessionid
|
14
|
+
response = Rack::Response.new(env)
|
15
|
+
configuration = {
|
16
|
+
account: Conjur.configuration.account,
|
17
|
+
stack: Conjur.configuration.stack,
|
18
|
+
appliance_url: Conjur.configuration.appliance_url,
|
19
|
+
login: Conjur::Authn.get_credentials[0]
|
20
|
+
}
|
21
|
+
response.status = 302
|
22
|
+
response.set_cookie('conjur_configuration', value: JSON.pretty_generate(configuration), path: '/')
|
23
|
+
response['Location'] = "/ui"
|
24
|
+
response.finish
|
25
|
+
else
|
26
|
+
[ 403, {}, ["Authorization is missing or invalid"] ]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def token_valid?(env)
|
33
|
+
token = extract_token(env)
|
34
|
+
if token == sessionid
|
35
|
+
sessionid
|
36
|
+
else
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def extract_token(env)
|
42
|
+
require 'cgi'
|
43
|
+
require 'uri'
|
44
|
+
query = URI.parse(env['REQUEST_URI']).query
|
45
|
+
query && ( sessionid = CGI.parse(query)['sessionid'] ) && sessionid[0]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2013 Conjur Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
# this software and associated documentation files (the "Software"), to deal in
|
6
|
+
# the Software without restriction, including without limitation the rights to
|
7
|
+
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
8
|
+
# the Software, and to permit persons to whom the Software is furnished to do so,
|
9
|
+
# subject to the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be included in all
|
12
|
+
# copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
16
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
17
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
18
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
19
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
20
|
+
#
|
21
|
+
|
22
|
+
module Conjur
|
23
|
+
module WebServer
|
24
|
+
# Launch a web server which serves local files and proxies to the remote Conjur API.
|
25
|
+
class Server
|
26
|
+
def start(root)
|
27
|
+
require 'rack'
|
28
|
+
require 'conjur/webserver/login'
|
29
|
+
require 'conjur/webserver/authorize'
|
30
|
+
require 'conjur/webserver/api_proxy'
|
31
|
+
require 'conjur/webserver/home'
|
32
|
+
require 'conjur/webserver/conjur_info'
|
33
|
+
require 'pry'
|
34
|
+
|
35
|
+
sessionid = self.sessionid
|
36
|
+
cookie_options = {
|
37
|
+
secret: SecureRandom.hex(32),
|
38
|
+
expire_after: 24*60*60
|
39
|
+
}
|
40
|
+
|
41
|
+
api_stack = [
|
42
|
+
[Rack::Session::Cookie, cookie_options],
|
43
|
+
#[Conjur::WebServer::Authorize, sessionid],
|
44
|
+
[Conjur::WebServer::ConjurInfo]
|
45
|
+
]
|
46
|
+
|
47
|
+
app = Rack::Builder.app do
|
48
|
+
map "/login" do
|
49
|
+
use Rack::Session::Cookie, cookie_options
|
50
|
+
run Conjur::WebServer::Login.new sessionid
|
51
|
+
end
|
52
|
+
map "/api" do
|
53
|
+
api_stack.each{|args| use *args}
|
54
|
+
run Conjur::WebServer::APIProxy.new
|
55
|
+
end
|
56
|
+
%w(js css fonts images).each do |path|
|
57
|
+
map "/#{path}" do
|
58
|
+
run Rack::File.new(File.join(root, path), 'Cache-Control' => 'max-age=0')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
map "/ui" do
|
62
|
+
run Conjur::WebServer::Home.new(root)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
options = {
|
66
|
+
app: app,
|
67
|
+
Port: port,
|
68
|
+
Threads: '0:64',
|
69
|
+
Verbose: true
|
70
|
+
}
|
71
|
+
|
72
|
+
# this vivifies the env for correct url settings
|
73
|
+
# otherwise puma sets it to development and
|
74
|
+
# confusion ensues
|
75
|
+
Conjur.configuration.env
|
76
|
+
|
77
|
+
Rack::Server.start(options)
|
78
|
+
end
|
79
|
+
|
80
|
+
def open
|
81
|
+
require 'launchy'
|
82
|
+
url = "http://localhost:#{port}/login?sessionid=#{sessionid}"
|
83
|
+
# as launchy sometimes silently fails, we need human-friendly failover
|
84
|
+
$stderr.puts "UI should be available now at #{url}"
|
85
|
+
Launchy.open(url)
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def port
|
91
|
+
@port ||= find_available_port
|
92
|
+
end
|
93
|
+
|
94
|
+
DEFAULT_PORT = 42_289
|
95
|
+
|
96
|
+
def find_available_port
|
97
|
+
begin
|
98
|
+
server = TCPServer.new('127.0.0.1', 0)
|
99
|
+
server.addr[1]
|
100
|
+
ensure
|
101
|
+
server.close if server
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def sessionid
|
106
|
+
require 'securerandom'
|
107
|
+
@sessionid ||= SecureRandom.hex(32)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|