# frozen_string_literal: true module API class GoProxy < ::API::Base helpers Gitlab::Golang helpers ::API::Helpers::PackagesHelpers # basic semver, except case encoded (A => !a) MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze before { require_packages_enabled! } helpers do def case_decode(str) # Converts "github.com/!azure" to "github.com/Azure" # # From `go help goproxy`: # # > To avoid problems when serving from case-sensitive file systems, # > the and elements are case-encoded, replacing # > every uppercase letter with an exclamation mark followed by the # > corresponding lower-case letter: github.com/Azure encodes as # > github.com/!azure. str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase } end def find_project!(id) # based on API::Helpers::Packages::BasicAuthHelpers#authorized_project_find! project = find_project(id) return project if project && can?(current_user, :read_project, project) if current_user not_found!('Project') else unauthorized! end end def find_module not_found! unless Feature.enabled?(:go_proxy, user_project) module_name = case_decode params[:module_name] bad_request!('Module Name') if module_name.blank? mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute not_found! unless mod mod end def find_version module_version = case_decode params[:module_version] ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version) not_found! unless ver&.valid? ver rescue ArgumentError not_found! end end params do requires :id, type: String, desc: 'The ID of a project' requires :module_name, type: String, desc: 'Module name', coerce_with: ->(val) { CGI.unescape(val) } end route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before do authorize_read_package! end namespace ':id/packages/go/*module_name/@v' do desc 'Get all tagged versions for a given Go module' do detail 'See `go help goproxy`, GET $GOPROXY//@v/list. This feature was introduced in GitLab 13.1.' end get 'list' do mod = find_module content_type 'text/plain' mod.versions.map { |t| t.name }.join("\n") end desc 'Get information about the given module version' do detail 'See `go help goproxy`, GET $GOPROXY//@v/.info. This feature was introduced in GitLab 13.1.' success ::API::Entities::GoModuleVersion end params do requires :module_version, type: String, desc: 'Module version' end get ':module_version.info', requirements: MODULE_VERSION_REQUIREMENTS do ver = find_version present ::Packages::Go::ModuleVersionPresenter.new(ver), with: ::API::Entities::GoModuleVersion end desc 'Get the module file of the given module version' do detail 'See `go help goproxy`, GET $GOPROXY//@v/.mod. This feature was introduced in GitLab 13.1.' end params do requires :module_version, type: String, desc: 'Module version' end get ':module_version.mod', requirements: MODULE_VERSION_REQUIREMENTS do ver = find_version content_type 'text/plain' ver.gomod end desc 'Get a zip of the source of the given module version' do detail 'See `go help goproxy`, GET $GOPROXY//@v/.zip. This feature was introduced in GitLab 13.1.' end params do requires :module_version, type: String, desc: 'Module version' end get ':module_version.zip', requirements: MODULE_VERSION_REQUIREMENTS do ver = find_version content_type 'application/zip' env['api.format'] = :binary header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: ver.name + '.zip') header['Content-Transfer-Encoding'] = 'binary' status :ok body ver.archive.string end end end end end