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: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ gem 'memoist'
gem "sinatra-cross_origin", "~> 0.3.1"

gem 'sinatra-session'

gem 'lru_redux'
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ GEM
httparty (0.16.1)
multi_xml (>= 0.5.2)
kgio (2.11.2)
lru_redux (0.8.4)
memoist (0.16.0)
method_source (0.9.0)
multi_xml (0.6.0)
Expand Down Expand Up @@ -48,6 +49,7 @@ PLATFORMS
DEPENDENCIES
foreman
httparty
lru_redux
memoist
oj
pry
Expand Down
7 changes: 6 additions & 1 deletion lib/config.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'lru_redux'

class Configuration

attr_accessor :registry_password,
Expand All @@ -14,7 +16,8 @@ class Configuration
:debug,
:login_allowed,
:title,
:session_secret
:session_secret,
:auth_type

def initialize
@registry_username = ENV['REGISTRY_USERNAME']
Expand All @@ -32,6 +35,7 @@ def initialize
@login_allowed = to_bool(ENV['ALLOW_REGISTRY_LOGIN'] || 'false')
@title = ENV['TITLE'] || "Crane Operator"
@session_secret = ENV['SESSION_SECRET'] || "insecure-session-secret!"
@auth_type = ENV['REGISTRY_AUTH_TYPE'] || "basic"
end

def to_bool(str)
Expand Down Expand Up @@ -59,6 +63,7 @@ def to_hash
:login_allowed => @login_allowed,
:title => @title,
:session_secret => @session_secret,
:auth_type => @auth_type,
}
end

Expand Down
93 changes: 84 additions & 9 deletions lib/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

module Helpers

@_cache = LruRedux::TTL::Cache.new(100, 5 * 60)

def self.cache
@_cache
end

def sort_versions(ary)
valid_version_numbers = ary.select { |i| i if i.match(/^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-[[:alnum:]]+)?$/) }
non_valid_version_numbers = ary - valid_version_numbers
Expand All @@ -24,11 +30,19 @@ def html(view)
File.read(File.join('public', "#{view.to_s}.html"))
end

def generateHeaders(config, session, headers={}, login={})
def generateHeaders(url, config, session, headers: {}, login: {}, auth_type: nil, method: 'get')
username = login[:username] || session[:username] || config.registry_username
password = login[:password] || session[:password] || config.registry_password
auth_type = auth_type || config.auth_type

if username
headers['Authorization'] = "Basic #{base64_docker_auth(username, password)}"
case auth_type
when "basic"
headers['Authorization'] = "Basic #{base64_docker_auth(username, password)}"
when "token"
token = getRegistryToken(url, config, session, login: login, method: method)
headers['Authorization'] = "Bearer #{token}"
end
end
return headers
end
Expand All @@ -41,8 +55,8 @@ def append_header(headers, addl_header)
headers.merge addl_header
end

def get(url, config, session, headers={}, query={})
response = HTTParty.get( "#{config.registry_url}#{url}", verify: config.ssl_verify, query: query, headers: generateHeaders(config, session, headers) )
def get(url, config, session, headers: {}, query: {})
response = http_get(url, config, session, headers: headers, query: query)
json = Oj.load response.body
if json['errors']
puts "Error talking to the docker registry!"
Expand All @@ -53,20 +67,81 @@ def get(url, config, session, headers={}, query={})
return json
end

def get_head(url, config, session, headers={})
HTTParty.head( "#{config.registry_url}#{url}", verify: config.ssl_verify, headers: generateHeaders(config, session, headers) )
def http_get(url, config, session, headers: {}, query: {}, login: {})
HTTParty.get(
"#{config.registry_url}#{url}",
verify: config.ssl_verify,
query: query,
headers: generateHeaders(url, config, session, headers: headers, login: login, method: 'get')
)
end

def send_delete(url, config, session, headers={})
HTTParty.delete( "#{config.registry_url}#{url}", verify: config.ssl_verify, headers: generateHeaders(config, session, headers) )
def http_head(url, config, session, headers: {})
HTTParty.head(
"#{config.registry_url}#{url}",
verify: config.ssl_verify,
headers: generateHeaders(url, config, session, headers: headers, method: 'head')
)
end

def http_delete(url, config, session, headers: {})
HTTParty.delete(
"#{config.registry_url}#{url}",
verify: config.ssl_verify,
headers: generateHeaders(url, config, session, headers: headers, method: 'delete')
)
end

def check_login(config, session, username=nil, password=nil, headers={})
if username.nil?
return false
end
login = {username: username, password: password}
response = HTTParty.get( "#{config.registry_url}/v2/_catalog", verify: config.ssl_verify, headers: generateHeaders(config, session, headers, login) )

response = http_get('/v2/_catalog', config, session, headers: headers, login: login)
return response.code != 401
end

def getRegistryToken(url, config, session, login: {}, method: 'get')
# enbrace the 401; the response headers tell us where the authorization service is
username = login[:username] || session[:username] || config.registry_username
cache_key = "#{username}__#{method}__#{url}"
token = Helpers.cache[cache_key.to_sym]
if !token
token = generateRegistryToken(url, config, session, login: login, method: method)
Helpers.cache[cache_key.to_sym] = token
end

return token
end

def generateRegistryToken(url, config, session, login: {}, method: 'get')
headers = generateHeaders(url, config, session, login: login, auth_type: 'none', method: method)
response = HTTParty.send(
method,
"#{config.registry_url}#{url}",
verify: config.ssl_verify,
headers: headers
)
www_auth = response.headers["www-authenticate"]
auth_url = www_auth.match(/realm="(?<auth_url>[^"]+)"/).captures[0]
service = www_auth.match(/service="([^"]+)"/).captures[0]
scope = www_auth.match(/scope="([^"]+)"/).captures[0]

# use basic auth against the auth service
jwt_response = HTTParty.get(
"#{auth_url}",
query: {
'service' => service,
'scope' => scope,
},
verify: config.ssl_verify,
headers: generateHeaders(url, config, session, headers: headers, login: login, auth_type: 'basic')
)
body = JSON.parse(jwt_response.body)

# relay our web token
return body["token"]

end
end
6 changes: 3 additions & 3 deletions server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def conf

def fetch_catalog(next_string=nil)
query={n: 100, last: next_string}
json = get("/v2/_catalog", conf, session, {}, query)
json = get("/v2/_catalog", conf, session, query: query)
if json['errors']
return json
end
Expand Down Expand Up @@ -98,13 +98,13 @@ def container_info(repo, manifest)
end

def fetch_digest(repo, manifest)
response = get_head("/v2/#{repo}/manifests/#{manifest}", conf, session, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
response = http_head("/v2/#{repo}/manifests/#{manifest}", conf, session, headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
return response.headers["docker-content-digest"]
end

def image_delete(repo, manifest)
digest = fetch_digest(repo, manifest)
return send_delete("/v2/#{repo}/manifests/#{digest}", conf, session, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
return http_delete("/v2/#{repo}/manifests/#{digest}", conf, session, headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
end

## Endpoints ##
Expand Down