Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby-version: ['2.7', '3.2', '3.3', '3.4']
ruby-version: ['3.3', '3.4', '4.0']

steps:
- uses: actions/checkout@v6
Expand Down
5 changes: 2 additions & 3 deletions cf-uaac.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,22 @@ Gem::Specification.new do |s|
s.description = %q{Client command line tools for interacting with the CloudFoundry User Account and Authorization (UAA) server. The UAA is an OAuth2 Authorization Server so it can be used by webapps and command line apps to obtain access tokens to act on behalf of users. The tokens can then be used to access protected resources in a Resource Server. This library can be used by clients (as a convenient wrapper for mainstream oauth gems) or by resource servers.}

s.license = 'Apache-2.0'
s.required_ruby_version = '>= 3.3'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ['lib']

# dependencies
s.add_runtime_dependency 'cf-uaa-lib', '~> 4.0.9'
s.add_runtime_dependency 'cf-uaa-lib', '~> 4.0.10'
s.add_development_dependency 'rake', '~> 13.0'
s.add_development_dependency 'rspec', '~> 3.12'
s.add_development_dependency 'simplecov', '~> 0.22.0'
s.add_development_dependency 'simplecov-rcov', '~> 0.3.1'
s.add_development_dependency 'ci_reporter', '~> 2.1.0'
s.add_development_dependency 'ci_reporter_rspec', '~> 1.0'
s.add_runtime_dependency 'highline', '>= 2', '< 4'
s.add_runtime_dependency 'eventmachine', '~> 1.2'
s.add_runtime_dependency 'launchy', '>= 2.5', '< 4.0'
s.add_runtime_dependency 'em-http-request', '~> 1.1', '>= 1.1.2'
s.add_runtime_dependency 'json', '~> 2.19', '>= 2.19.3'
s.add_runtime_dependency 'rack', '~> 3.2', '>= 3.2.5'
end
143 changes: 73 additions & 70 deletions lib/uaa/stub/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# subcomponent's license, as noted in the LICENSE file.
#++

require 'eventmachine'
require 'socket'
require 'date'
require 'logger'
require 'pp'
Expand All @@ -33,7 +33,6 @@ def initialize; @state, @prelude = :init, "" end
private

def bslice(str, range)
# byteslice is available in ruby 1.9.3
str.respond_to?(:byteslice) ? str.byteslice(range) : str.slice(range)
end

Expand Down Expand Up @@ -64,7 +63,7 @@ def add_lines(str)

public

# adds data to the request, returns true if request is complete
# adds data to the request, returns truthy if request is complete
def completed?(str)
str, @prelude = @prelude + str, "" unless @prelude.empty?
add_lines(str)
Expand Down Expand Up @@ -136,7 +135,6 @@ class Base
attr_accessor :request, :reply, :match, :server

def self.route(http_methods, matcher, filters = {}, &handler)
fail unless !EM.reactor_running? || EM.reactor_thread?
matcher = Regexp.new("^#{Regexp.escape(matcher.to_s)}$") unless matcher.is_a?(Regexp)
filters = filters.each_with_object({}) { |(k, v), o|
o[k.downcase] = v.is_a?(Regexp) ? v : Regexp.new("^#{Regexp.escape(v.to_s)}$")
Expand All @@ -156,7 +154,6 @@ def self.route(http_methods, matcher, filters = {}, &handler)
end

def self.find_route(request)
fail unless EM.reactor_thread?
if @routes && (rary = @routes[request.method])
rary.each { |r; m|
next unless (m = r[0].match(request.path))
Expand Down Expand Up @@ -204,44 +201,60 @@ def reply_in_kind(status = nil, info)

end

#------------------------------------------------------------------------------
module Connection
attr_accessor :req_handler
def unbind; req_handler.server.delete_connection(self) end

def receive_data(data)
#req_handler.server.logger.debug "got #{data.bytesize} bytes: #{data.inspect}"
return unless req_handler.request.completed? data
req_handler.process
send_data req_handler.reply.to_s
if req_handler.reply.headers['connection'] =~ /^close$/i || req_handler.server.status != :running
close_connection_after_writing
end
rescue Exception => e
req_handler.server.logger.debug "exception from receive_data: #{e.message}"
req_handler.server.trace { e.backtrace }
close_connection
end
end

#------------------------------------------------------------------------------
class Server

private

def done
fail unless @connections.empty?
EM.stop if @em_thread && EM.reactor_running?
@connections, @status, @sig, @em_thread = [], :stopped, nil, nil
sleep 0.1 unless EM.reactor_thread? # give EM a chance to stop
logger.debug EM.reactor_running?? "server done but EM still running": "server really done"
# Handle one TCP client socket: parse requests, dispatch, write replies.
# Supports keep-alive: the Request object resets itself after each
# completed? call, so the same req_handler is reused for pipelined requests.
def handle_client(socket)
req_handler = @req_handler.new(self)
loop do
# If leftover bytes from the previous request already complete the next
# one (pipelining / chunked reads), process without a blocking read.
unless req_handler.request.completed?("")
begin
data = socket.readpartial(4096)
rescue EOFError, Errno::ECONNRESET
break
end
next unless req_handler.request.completed?(data)
end
req_handler.process
socket.write(req_handler.reply.to_s)
break if req_handler.reply.headers['connection'] =~ /^close$/i || @status != :running
end
rescue Errno::ECONNRESET, Errno::EPIPE, IOError => e
logger.debug "connection error: #{e.message}"
ensure
socket.close rescue nil
@mutex.synchronize { @connections.delete(socket) }
logger.debug "connection closed"
end

def initialize_connection(conn)
logger.debug "starting connection"
fail unless EM.reactor_thread?
@connections << conn
conn.req_handler, conn.comm_inactivity_timeout = @req_handler.new(self), 30
# Accept connections in a loop until the server is stopped or the listening
# socket is closed.
def accept_loop
loop do
begin
socket = @tcp_server.accept_nonblock
@mutex.synchronize { @connections << socket }
logger.debug "starting connection"
Thread.new(socket) { |s| handle_client(s) }
rescue IO::WaitReadable, Errno::EINTR
IO.select([@tcp_server], nil, nil, 0.5) rescue nil
break if @status != :running
rescue Errno::EBADF, IOError
break
end
end
rescue => e
logger.debug "accept loop error: #{e.message}" unless e.is_a?(IOError) || e.is_a?(Errno::EBADF)
ensure
@status = :stopped
logger.debug "server really done"
end

public
Expand All @@ -258,64 +271,54 @@ def initialize(req_handler, options)
@host = options[:host] || "localhost"
@init_port = options[:port] || 0
@root = options[:root]
@connections, @status, @sig, @em_thread = [], :stopped, nil, nil
@connections = []
@mutex = Mutex.new
@status = :stopped
@server_thread = nil
end

def start
raise ArgumentError, "attempt to start a server that's already running" unless @status == :stopped
logger.debug "starting #{self.class} server #{@host}"
EM.schedule do
@sig = EM.start_server(@host, @init_port, Connection) { |c| initialize_connection(c) }
@port = Socket.unpack_sockaddr_in(EM.get_sockname(@sig))[0]
logger.info "#{self.class} server started at #{url}"
end
@tcp_server = TCPServer.new(@host, @init_port)
@port = @tcp_server.addr[1]
logger.info "#{self.class} server started at #{url}"
@status = :running
self
end

# Start the server and run the accept loop on a background thread.
# Returns immediately; caller can use #url and #port right away.
def run_on_thread
raise ArgumentError, "can't run on thread, EventMachine already running" if EM.reactor_running?
logger.debug { "starting eventmachine on thread" }
cthred = Thread.current
@em_thread = Thread.new do
begin
EM.run { start; cthred.run }
logger.debug "server thread done"
rescue Exception => e
logger.debug { "unhandled exception on stub server thread: #{e.message}" }
trace { e.backtrace }
raise
end
end
Thread.stop
raise ArgumentError, "can't run on thread, server already running" if @status == :running
logger.debug "starting server on thread"
start
@server_thread = Thread.new { accept_loop }
logger.debug "running on thread"
self
end

# Start the server and run the accept loop on the calling thread (blocking).
def run
raise ArgumentError, "can't run, EventMachine already running" if EM.reactor_running?
@em_thread = Thread.current
EM.run { start }
logger.debug "server and event machine done"
raise ArgumentError, "can't run, server already running" if @status == :running
@server_thread = Thread.current
start
accept_loop
logger.debug "server and event loop done"
end

# if on reactor thread, start shutting down but return if connections still
# in process, and let them disconnect when complete -- server is not really
# done until it's status is stopped.
# if not on reactor thread, wait until everything's cleaned up and stopped
# Stop accepting new connections, close the listening socket, and wait for
# the accept loop thread to mark status :stopped.
def stop
logger.debug "stopping server"
@status = :stopping
EM.stop_server @sig
done if @connections.empty?
sleep 0.1 while @status != :stopped unless EM.reactor_thread?
@tcp_server.close rescue nil
sleep 0.05 while @status != :stopped
end

def delete_connection(conn)
logger.debug "deleting connection"
fail unless EM.reactor_thread?
@connections.delete(conn)
done if @status != :running && @connections.empty?
@mutex.synchronize { @connections.delete(conn) }
end

end
Expand Down
6 changes: 3 additions & 3 deletions lib/uaa/stub/uaa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -466,12 +466,12 @@ def obj_access?(rtype, oid, perm)
route :get, %r{^/Groups/External/list(\?|$)(.*)} do
return unless valid_token('scim.read')

query_params = CGI::parse(match[2])
query_params = URI.decode_www_form(match[2]).to_h

start_index_param = query_params['startIndex'].first
start_index_param = query_params['startIndex'] || ''
start_index = start_index_param.empty? ? 1 : start_index_param.to_i

count_param = query_params['count'].first
count_param = query_params['count'] || ''
count = count_param.empty? ? 100 : count_param.to_i

group_mappings = server.scim.get_group_mappings
Expand Down
Loading
Loading