Skip to content

Commit

Permalink
feat: 支持git proxy #1122 (#1159)
Browse files Browse the repository at this point in the history
* feat: 实现代理仓库 #1122

* feat: 增加云研发源ip限制 #1122

* feat: 处理ip转换 #1122

* feat: 修复大仓库下的推送报错 #1122

* feat: 支持查询代理仓库接口 #1122

* feat: 支持git lfs #1122

* feat: merge master #1122

* feat: 修复单元测试 #1122
  • Loading branch information
felixncheng authored Sep 21, 2023
1 parent ad0ca30 commit 3bc6c37
Show file tree
Hide file tree
Showing 24 changed files with 524 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ enum class RepositoryCategory {
LOCAL, // 本地存储仓库
REMOTE, // 远程仓库,一般是代理,例如Maven
VIRTUAL, // 虚拟仓库一般是用来聚合其他仓库
COMPOSITE // 组合类型仓库
COMPOSITE, // 组合类型仓库
PROXY, // 代理仓库,完全代理
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.tencent.bkrepo.common.artifact.pojo.configuration.composite.CompositeConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.local.LocalConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.proxy.ProxyConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.RemoteConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.virtual.VirtualConfiguration
import io.swagger.annotations.ApiModelProperty
Expand All @@ -51,7 +52,8 @@ import io.swagger.annotations.ApiModelProperty
JsonSubTypes.Type(value = LocalConfiguration::class, name = "rpm-local"), // 兼容处理
JsonSubTypes.Type(value = RemoteConfiguration::class, name = RemoteConfiguration.type),
JsonSubTypes.Type(value = VirtualConfiguration::class, name = VirtualConfiguration.type),
JsonSubTypes.Type(value = CompositeConfiguration::class, name = CompositeConfiguration.type)
JsonSubTypes.Type(value = CompositeConfiguration::class, name = CompositeConfiguration.type),
JsonSubTypes.Type(value = ProxyConfiguration::class, name = ProxyConfiguration.type)
)
open class RepositoryConfiguration {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.tencent.bkrepo.common.artifact.pojo.configuration.proxy

import com.tencent.bkrepo.common.artifact.pojo.configuration.composite.ProxyChannelSetting
import com.tencent.bkrepo.common.artifact.pojo.configuration.local.LocalConfiguration
import io.swagger.annotations.ApiModel
import io.swagger.annotations.ApiModelProperty

/**
* 代理仓库配置
* */
@ApiModel("代理仓库配置")
class ProxyConfiguration(
/**
* 代理服务器配置
* */
@ApiModelProperty("代理服务器配置")
val proxy: ProxyChannelSetting = ProxyChannelSetting(false, "", ""),
/**
* 客户端访问的代理地址
* */
@ApiModelProperty("访问url", required = false)
var url: String? = null,
) : LocalConfiguration() {
companion object {
const val type = "proxy"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ package com.tencent.bkrepo.common.artifact.repository
import com.tencent.bkrepo.common.artifact.config.ArtifactBeanRegistrar
import com.tencent.bkrepo.common.artifact.repository.composite.CompositeRepository
import com.tencent.bkrepo.common.artifact.repository.context.ArtifactContextHolder
import com.tencent.bkrepo.common.artifact.repository.proxy.ProxyRepository
import com.tencent.bkrepo.common.artifact.repository.redirect.CosRedirectService
import com.tencent.bkrepo.common.artifact.repository.redirect.DownloadRedirectManager
import com.tencent.bkrepo.common.artifact.repository.redirect.EdgeNodeRedirectService
Expand All @@ -45,6 +46,7 @@ import org.springframework.context.annotation.Import
ArtifactBeanRegistrar::class,
ArtifactContextHolder::class,
CompositeRepository::class,
ProxyRepository::class,
EdgeNodeRedirectService::class,
CosRedirectService::class,
DownloadRedirectManager::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.tencent.bkrepo.common.artifact.constant.REPO_KEY
import com.tencent.bkrepo.common.artifact.pojo.RepositoryCategory
import com.tencent.bkrepo.common.artifact.pojo.configuration.composite.CompositeConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.local.LocalConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.proxy.ProxyConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.remote.RemoteConfiguration
import com.tencent.bkrepo.common.artifact.pojo.configuration.virtual.VirtualConfiguration
import com.tencent.bkrepo.common.security.util.SecurityUtils
Expand Down Expand Up @@ -174,4 +175,15 @@ open class ArtifactContext(
require(this.repositoryDetail.category == RepositoryCategory.COMPOSITE)
return this.repositoryDetail.configuration as CompositeConfiguration
}

/**
* 获取代理仓库配置
*
* 当仓库类型和配置类型不符时抛[IllegalArgumentException]异常
*/
@Throws(IllegalArgumentException::class)
fun getProxyConfiguration(): ProxyConfiguration {
require(this.repositoryDetail.category == RepositoryCategory.PROXY)
return this.repositoryDetail.configuration as ProxyConfiguration
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.tencent.bkrepo.common.artifact.pojo.RepositoryCategory
import com.tencent.bkrepo.common.artifact.pojo.RepositoryType
import com.tencent.bkrepo.common.artifact.repository.composite.CompositeRepository
import com.tencent.bkrepo.common.artifact.repository.core.ArtifactRepository
import com.tencent.bkrepo.common.artifact.repository.proxy.ProxyRepository
import com.tencent.bkrepo.common.security.http.core.HttpAuthSecurity
import com.tencent.bkrepo.common.service.util.HttpContextHolder
import com.tencent.bkrepo.common.storage.core.config.RateLimitProperties
Expand All @@ -64,6 +65,7 @@ import javax.servlet.http.HttpServletRequest
class ArtifactContextHolder(
artifactConfigurers: List<ArtifactConfigurer>,
compositeRepository: CompositeRepository,
proxyRepository: ProxyRepository,
repositoryClient: RepositoryClient,
nodeClient: NodeClient,
private val httpAuthSecurity: ObjectProvider<HttpAuthSecurity>
Expand All @@ -72,6 +74,7 @@ class ArtifactContextHolder(
init {
Companion.artifactConfigurers = artifactConfigurers
Companion.compositeRepository = compositeRepository
Companion.proxyRepository = proxyRepository
Companion.repositoryClient = repositoryClient
Companion.nodeClient = nodeClient
Companion.httpAuthSecurity = httpAuthSecurity
Expand All @@ -84,6 +87,7 @@ class ArtifactContextHolder(
companion object {
private lateinit var artifactConfigurers: List<ArtifactConfigurer>
private lateinit var compositeRepository: CompositeRepository
private lateinit var proxyRepository: ProxyRepository
private lateinit var repositoryClient: RepositoryClient
private lateinit var nodeClient: NodeClient
private lateinit var httpAuthSecurity: ObjectProvider<HttpAuthSecurity>
Expand Down Expand Up @@ -131,6 +135,7 @@ class ArtifactContextHolder(
RepositoryCategory.REMOTE -> currentArtifactConfigurer.getRemoteRepository()
RepositoryCategory.VIRTUAL -> currentArtifactConfigurer.getVirtualRepository()
RepositoryCategory.COMPOSITE -> compositeRepository
RepositoryCategory.PROXY -> proxyRepository
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.tencent.bkrepo.common.artifact.repository.proxy

import com.tencent.bkrepo.common.artifact.repository.core.AbstractArtifactRepository
import org.springframework.stereotype.Service

/**
* 代理仓库
*
* 暂时不做任务业务处理,仅用作区分仓库类型
* */
@Service
class ProxyRepository : AbstractArtifactRepository()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tencent.bkrepo.common.service.util.proxy

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import okhttp3.Response

open class DefaultProxyCallHandler : ProxyCallHandler {
override fun after(proxyRequest: HttpServletRequest, proxyResponse: HttpServletResponse, response: Response) {
// 转发状态码
proxyResponse.status = response.code
// 转发头
response.headers.forEach { (key, value) -> proxyResponse.addHeader(key, value) }

// 转发body
response.body?.byteStream()?.use {
it.copyTo(proxyResponse.outputStream)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.tencent.bkrepo.common.service.util.proxy

import com.tencent.bkrepo.common.api.constant.BASIC_AUTH_PREFIX
import com.tencent.bkrepo.common.api.constant.HttpHeaders
import com.tencent.bkrepo.common.api.util.BasicAuthUtils
import com.tencent.bkrepo.common.service.util.okhttp.HttpClientBuilderFactory
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okio.BufferedSink
import okio.source
import org.slf4j.LoggerFactory
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

object HttpProxyUtil {
private val logger = LoggerFactory.getLogger(HttpProxyUtil::class.java)
private val client = HttpClientBuilderFactory.create().build()
private val defaultProxyCallHandler = DefaultProxyCallHandler()
fun proxy(
proxyRequest: HttpServletRequest,
proxyResponse: HttpServletResponse,
targetUrl: String,
prefix: String? = null,
proxyCallHandler: ProxyCallHandler = defaultProxyCallHandler,
) {
val newUrl = "$targetUrl${proxyRequest.requestURI.removePrefix(prefix.orEmpty())}?${proxyRequest.queryString}"
val newRequest = Request.Builder()
.url(newUrl)
.apply {
proxyRequest.headers().forEach { (key, value) -> this.header(key, value) }
}
.method(proxyRequest.method, proxyRequest.body())
.build()
val newResponse = client
.newCall(newRequest)
.execute()
proxyRequest.accessLog(newResponse)
proxyCallHandler.after(proxyRequest, proxyResponse, newResponse)
}

fun HttpServletRequest.headers(): Map<String, String> {
val headers = mutableMapOf<String, String>()
val headerNames = this.headerNames
while (headerNames.hasMoreElements()) {
val headerName = headerNames.nextElement()
headers[headerName] = this.getHeader(headerName)
}
return headers
}

fun HttpServletRequest.body(): RequestBody? {
val isChunked = headers()[HttpHeaders.TRANSFER_ENCODING] == "chunked"
if (this.contentLengthLong <= 0 && !isChunked) {
return null
}
val mediaType = this.contentType?.toMediaTypeOrNull()
val inputStream = this.inputStream
val contentLength = this.contentLengthLong
return object : RequestBody() {
override fun contentType(): MediaType? = mediaType

override fun contentLength(): Long = contentLength

override fun writeTo(sink: BufferedSink) {
inputStream.source().use {
sink.writeAll(it)
}
}
}
}

private fun HttpServletRequest.accessLog(upRes: Response) {
var user = "-"
if (getHeader(HttpHeaders.AUTHORIZATION).orEmpty().startsWith(BASIC_AUTH_PREFIX)) {
val authorizationHeader = getHeader(HttpHeaders.AUTHORIZATION).orEmpty()
user = BasicAuthUtils.decode(authorizationHeader).first
}
val requestTime = System.currentTimeMillis() - upRes.sentRequestAtMillis
val httpUserAgent = getHeader(HttpHeaders.USER_AGENT)
val url = upRes.request.url.host
val requestBodyBytes = contentLengthLong
logger.info(
"\"$method $requestURI $protocol\" - " +
"user:$user up_status: ${upRes.code} ms:$requestTime up:$url agent:$httpUserAgent $requestBodyBytes",
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tencent.bkrepo.common.service.util.proxy

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import okhttp3.Response

interface ProxyCallHandler {
fun after(proxyRequest: HttpServletRequest, proxyResponse: HttpServletResponse, response: Response)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.tencent.bkrepo.git.config

import com.tencent.bkrepo.git.constant.HubType
import com.tencent.bkrepo.git.interceptor.devx.DevxProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.NestedConfigurationProperty

Expand All @@ -9,7 +10,9 @@ data class GitProperties(
var storageCredentialsKey: String? = null,
var locationDir: String? = null,
@NestedConfigurationProperty
var hub: Hub = Hub()
var hub: Hub = Hub(),
@NestedConfigurationProperty
var devx: DevxProperties = DevxProperties()
) {

data class Hub(var github: String? = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.tencent.bkrepo.git.config

import com.tencent.bkrepo.git.artifact.GitRepoInterceptor
import com.tencent.bkrepo.git.interceptor.ContextSettingInterceptor
import com.tencent.bkrepo.git.interceptor.devx.DevxSrcIpInterceptor
import com.tencent.bkrepo.git.interceptor.ProxyInterceptor
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -20,9 +22,18 @@ class GitWebConfig : WebMvcConfigurer {
registry.addInterceptor(ContextSettingInterceptor())
.addPathPatterns("/**")
.order(Ordered.LOWEST_PRECEDENCE)
registry.addInterceptor(ProxyInterceptor())
.addPathPatterns("/**")
.order(Ordered.HIGHEST_PRECEDENCE + 1)
registry.addInterceptor(devxSrcIpInterceptor())
.addPathPatterns("/**")
.order(Ordered.HIGHEST_PRECEDENCE)
super.addInterceptors(registry)
}

@Bean
fun repoInterceptor() = GitRepoInterceptor()

@Bean
fun devxSrcIpInterceptor() = DevxSrcIpInterceptor()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.tencent.bkrepo.git.controller

import com.tencent.bkrepo.common.api.exception.MethodNotAllowedException
import com.tencent.bkrepo.common.api.pojo.Response
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController

@RequestMapping("{projectId}/{repoName}.git")
@RestController
class GitLfsController {

@PostMapping("/info/lfs/objects/batch")
fun batch(
@PathVariable projectId: String,
@PathVariable repoName: String,
): Response<Void> {
throw MethodNotAllowedException()
}

@GetMapping()
@RequestMapping("/content/lfs/objects/{oid}", method = [RequestMethod.GET, RequestMethod.PUT])
fun get(@PathVariable oid: String): Response<Void> {
throw MethodNotAllowedException()
}
}
Loading

0 comments on commit 3bc6c37

Please sign in to comment.