mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5207082cd1 | |||
| 608b50f18a |
@@ -75,8 +75,10 @@ func claimsFromToken(tokenString string) (jwt.MapClaims, bool) {
|
|||||||
if subUser, _ := claims["sub_user"].(string); subUser != "" {
|
if subUser, _ := claims["sub_user"].(string); subUser != "" {
|
||||||
tokenVersionFloat, hasVersion := claims["token_version"].(float64)
|
tokenVersionFloat, hasVersion := claims["token_version"].(float64)
|
||||||
tokenVersion := int(tokenVersionFloat)
|
tokenVersion := int(tokenVersionFloat)
|
||||||
|
foundSubUser := false
|
||||||
for i := range config.AppConfig.SubUsers {
|
for i := range config.AppConfig.SubUsers {
|
||||||
if config.AppConfig.SubUsers[i].Username == subUser {
|
if config.AppConfig.SubUsers[i].Username == subUser {
|
||||||
|
foundSubUser = true
|
||||||
stored := config.AppConfig.SubUsers[i].TokenVersion
|
stored := config.AppConfig.SubUsers[i].TokenVersion
|
||||||
// If stored version > 0, require token_version to match exactly.
|
// If stored version > 0, require token_version to match exactly.
|
||||||
// This also rejects legacy tokens that lack token_version entirely.
|
// This also rejects legacy tokens that lack token_version entirely.
|
||||||
@@ -86,6 +88,9 @@ func claimsFromToken(tokenString string) (jwt.MapClaims, bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !foundSubUser {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, ok
|
return claims, ok
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import (
|
|||||||
type webVNCTicket struct {
|
type webVNCTicket struct {
|
||||||
ContainerName string
|
ContainerName string
|
||||||
ContainerUUID string
|
ContainerUUID string
|
||||||
|
Username string
|
||||||
SubUser bool
|
SubUser bool
|
||||||
|
ClientIP string
|
||||||
|
UserAgent string
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +61,17 @@ func HandleVNCTicket(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
username, isSubUser := vncRequesterIdentity(r)
|
||||||
ticket := randomHex(32)
|
ticket := randomHex(32)
|
||||||
webVNCTickets.Lock()
|
webVNCTickets.Lock()
|
||||||
cleanupExpiredWebVNCTicketsLocked(time.Now())
|
cleanupExpiredWebVNCTicketsLocked(time.Now())
|
||||||
webVNCTickets.items[ticket] = webVNCTicket{
|
webVNCTickets.items[ticket] = webVNCTicket{
|
||||||
ContainerName: c.Name,
|
ContainerName: c.Name,
|
||||||
ContainerUUID: c.UUID,
|
ContainerUUID: c.UUID,
|
||||||
SubUser: isSubUserRequest(r),
|
Username: username,
|
||||||
|
SubUser: isSubUser,
|
||||||
|
ClientIP: clientIP(r),
|
||||||
|
UserAgent: r.UserAgent(),
|
||||||
ExpiresAt: time.Now().Add(60 * time.Second),
|
ExpiresAt: time.Now().Add(60 * time.Second),
|
||||||
}
|
}
|
||||||
webVNCTickets.Unlock()
|
webVNCTickets.Unlock()
|
||||||
@@ -89,7 +96,7 @@ func HandleVNCProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
item, ok := consumeWebVNCTicket(ticket, containerName)
|
item, ok := consumeWebVNCTicket(ticket, containerName, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "invalid or expired ticket", http.StatusUnauthorized)
|
http.Error(w, "invalid or expired ticket", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
@@ -137,7 +144,7 @@ func HandleVNCProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
defer ws.Close()
|
defer ws.Close()
|
||||||
|
|
||||||
log.Printf("WebVNC connected for container %s -> 127.0.0.1:%d", containerName, vncPort)
|
log.Printf("WebVNC connected for container %s as %s (sub_user=%t) -> 127.0.0.1:%d", containerName, item.Username, item.SubUser, vncPort)
|
||||||
|
|
||||||
done := make(chan string, 2)
|
done := make(chan string, 2)
|
||||||
var writeMu sync.Mutex
|
var writeMu sync.Mutex
|
||||||
@@ -147,7 +154,21 @@ func HandleVNCProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
reason := <-done
|
reason := <-done
|
||||||
_ = vncConn.Close()
|
_ = vncConn.Close()
|
||||||
_ = ws.Close()
|
_ = ws.Close()
|
||||||
log.Printf("WebVNC disconnected for container %s: %s", containerName, reason)
|
log.Printf("WebVNC disconnected for container %s as %s: %s", containerName, item.Username, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func vncRequesterIdentity(r *http.Request) (string, bool) {
|
||||||
|
claims, ok := claimsFromRequest(r)
|
||||||
|
if !ok {
|
||||||
|
return "api-key", false
|
||||||
|
}
|
||||||
|
if subUser, ok := claims["sub_user"].(string); ok && subUser != "" {
|
||||||
|
return subUser, true
|
||||||
|
}
|
||||||
|
if username, ok := claims["username"].(string); ok && username != "" {
|
||||||
|
return username, false
|
||||||
|
}
|
||||||
|
return "unknown", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func webVNCTicketFromRequest(r *http.Request) string {
|
func webVNCTicketFromRequest(r *http.Request) string {
|
||||||
@@ -175,7 +196,7 @@ func webVNCResponseProtocol(r *http.Request) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func consumeWebVNCTicket(ticket, containerName string) (webVNCTicket, bool) {
|
func consumeWebVNCTicket(ticket, containerName string, r *http.Request) (webVNCTicket, bool) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
webVNCTickets.Lock()
|
webVNCTickets.Lock()
|
||||||
defer webVNCTickets.Unlock()
|
defer webVNCTickets.Unlock()
|
||||||
@@ -185,7 +206,10 @@ func consumeWebVNCTicket(ticket, containerName string) (webVNCTicket, bool) {
|
|||||||
return webVNCTicket{}, false
|
return webVNCTicket{}, false
|
||||||
}
|
}
|
||||||
delete(webVNCTickets.items, ticket)
|
delete(webVNCTickets.items, ticket)
|
||||||
return item, item.ContainerName == containerName && now.Before(item.ExpiresAt)
|
return item, item.ContainerName == containerName &&
|
||||||
|
item.ClientIP == clientIP(r) &&
|
||||||
|
item.UserAgent == r.UserAgent() &&
|
||||||
|
now.Before(item.ExpiresAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupExpiredWebVNCTicketsLocked(now time.Time) {
|
func cleanupExpiredWebVNCTicketsLocked(now time.Time) {
|
||||||
|
|||||||
@@ -993,6 +993,27 @@ func parseSubIDRange(path, user string) (int, error) {
|
|||||||
return 0, fmt.Errorf("%s must contain a %s subordinate id range with at least 65536 ids", path, user)
|
return 0, fmt.Errorf("%s must contain a %s subordinate id range with at least 65536 ids", path, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ensureUnprivilegedLXCPathAccess(lxcName string) error {
|
||||||
|
// Unprivileged container root maps to a subordinate host UID, so it needs
|
||||||
|
// execute permission on the LXC parent and container directories to reach
|
||||||
|
// rootfs. Some distributions create /var/lib/lxc as 750/700, which causes
|
||||||
|
// lxc-start to abort with "Could not access /var/lib/lxc".
|
||||||
|
for _, path := range []string{m.LxcPath, filepath.Join(m.LxcPath, lxcName)} {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mode := info.Mode().Perm()
|
||||||
|
if mode&0001 != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.Chmod(path, mode|0001); err != nil {
|
||||||
|
return fmt.Errorf("failed to fix LXC path permissions for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) shiftRootfsForUnprivileged(lxcName string) error {
|
func (m *Manager) shiftRootfsForUnprivileged(lxcName string) error {
|
||||||
uidBase, gidBase, err := unprivilegedIDMap()
|
uidBase, gidBase, err := unprivilegedIDMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1000,6 +1021,9 @@ func (m *Manager) shiftRootfsForUnprivileged(lxcName string) error {
|
|||||||
}
|
}
|
||||||
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
||||||
marker := filepath.Join(rootfsPath, ".clicd-unprivileged-shifted")
|
marker := filepath.Join(rootfsPath, ".clicd-unprivileged-shifted")
|
||||||
|
if err := m.ensureUnprivilegedLXCPathAccess(lxcName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := os.Stat(marker); err == nil {
|
if _, err := os.Stat(marker); err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "1.1.3"
|
Version = "1.1.4"
|
||||||
Repo = "MengMengCode/CLICD"
|
Repo = "MengMengCode/CLICD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "clicd-frontend",
|
"name": "clicd-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -21,6 +21,46 @@ export default function WebVNCViewer({ containerName, onClose }: WebVNCViewerPro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ensureResizeObserver = () => {
|
||||||
|
if ('ResizeObserver' in window) return
|
||||||
|
|
||||||
|
class FallbackResizeObserver {
|
||||||
|
private target: Element | null = null
|
||||||
|
private timer = 0
|
||||||
|
private lastWidth = -1
|
||||||
|
private lastHeight = -1
|
||||||
|
|
||||||
|
constructor(private callback: ResizeObserverCallback) {}
|
||||||
|
|
||||||
|
observe = (target: Element) => {
|
||||||
|
this.target = target
|
||||||
|
this.check()
|
||||||
|
this.timer = window.setInterval(this.check, 250)
|
||||||
|
window.addEventListener('resize', this.check)
|
||||||
|
}
|
||||||
|
|
||||||
|
unobserve = () => this.disconnect()
|
||||||
|
|
||||||
|
disconnect = () => {
|
||||||
|
if (this.timer) window.clearInterval(this.timer)
|
||||||
|
this.timer = 0
|
||||||
|
window.removeEventListener('resize', this.check)
|
||||||
|
this.target = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private check = () => {
|
||||||
|
if (!this.target) return
|
||||||
|
const contentRect = this.target.getBoundingClientRect()
|
||||||
|
if (contentRect.width === this.lastWidth && contentRect.height === this.lastHeight) return
|
||||||
|
this.lastWidth = contentRect.width
|
||||||
|
this.lastHeight = contentRect.height
|
||||||
|
this.callback([{ target: this.target, contentRect } as ResizeObserverEntry], this as unknown as ResizeObserver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(window as unknown as { ResizeObserver: typeof ResizeObserver }).ResizeObserver = FallbackResizeObserver as unknown as typeof ResizeObserver
|
||||||
|
}
|
||||||
|
|
||||||
const connect = async () => {
|
const connect = async () => {
|
||||||
const target = screenRef.current
|
const target = screenRef.current
|
||||||
if (!target) return
|
if (!target) return
|
||||||
@@ -47,7 +87,10 @@ export default function WebVNCViewer({ containerName, onClose }: WebVNCViewerPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rfb = new RFB(target, getWebVNCUrl(containerName, ticket))
|
ensureResizeObserver()
|
||||||
|
const rfb = new RFB(target, getWebVNCUrl(containerName), {
|
||||||
|
wsProtocols: ['binary', `clicd-vnc-ticket.${ticket}`],
|
||||||
|
})
|
||||||
rfb.scaleViewport = true
|
rfb.scaleViewport = true
|
||||||
rfb.resizeSession = false
|
rfb.resizeSession = false
|
||||||
rfb.focusOnClick = true
|
rfb.focusOnClick = true
|
||||||
@@ -76,7 +119,8 @@ export default function WebVNCViewer({ containerName, onClose }: WebVNCViewerPro
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
setStatus('error')
|
setStatus('error')
|
||||||
setErrorMsg('WebVNC 初始化失败')
|
const message = err instanceof Error && err.message ? `:${err.message}` : ''
|
||||||
|
setErrorMsg(`WebVNC 初始化失败${message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function Login() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.3</p>
|
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.4</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -456,10 +456,9 @@ export const getWebSSHUrl = (containerName: string) => {
|
|||||||
return `${protocol}//${window.location.host}/api/ssh?${params.toString()}`
|
return `${protocol}//${window.location.host}/api/ssh?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getWebVNCUrl = (containerName: string, ticket?: string) => {
|
export const getWebVNCUrl = (containerName: string) => {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||||
const params = new URLSearchParams({ container: containerName })
|
const params = new URLSearchParams({ container: containerName })
|
||||||
if (ticket) params.set('ticket', ticket)
|
|
||||||
return `${protocol}//${window.location.host}/api/vnc?${params.toString()}`
|
return `${protocol}//${window.location.host}/api/vnc?${params.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -784,6 +784,12 @@ setup_subids() {
|
|||||||
grep -q '^root:' /etc/subgid 2>/dev/null || echo 'root:100000:65536' >> /etc/subgid
|
grep -q '^root:' /etc/subgid 2>/dev/null || echo 'root:100000:65536' >> /etc/subgid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure_lxc_storage_access() {
|
||||||
|
log "Configuring LXC storage directory permissions..."
|
||||||
|
mkdir -p /var/lib/lxc
|
||||||
|
chmod 755 /var/lib/lxc
|
||||||
|
}
|
||||||
|
|
||||||
try_enable_project_quota() {
|
try_enable_project_quota() {
|
||||||
root_src="$(findmnt -no SOURCE / 2>/dev/null || true)"
|
root_src="$(findmnt -no SOURCE / 2>/dev/null || true)"
|
||||||
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || true)"
|
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || true)"
|
||||||
@@ -967,6 +973,7 @@ run_step "配置内核网络参数" configure_kernel_networking
|
|||||||
run_step "配置运行时服务" setup_runtime_services
|
run_step "配置运行时服务" setup_runtime_services
|
||||||
run_step "配置 libvirt default NAT 网络" setup_default_libvirt_network
|
run_step "配置 libvirt default NAT 网络" setup_default_libvirt_network
|
||||||
run_step "配置 UID/GID 映射" setup_subids
|
run_step "配置 UID/GID 映射" setup_subids
|
||||||
|
run_step "Configure LXC storage permissions" configure_lxc_storage_access
|
||||||
run_step "检查 project quota" try_enable_project_quota
|
run_step "检查 project quota" try_enable_project_quota
|
||||||
run_step "下载发行版包" download_release_if_needed
|
run_step "下载发行版包" download_release_if_needed
|
||||||
run_step "安装 CLICD 二进制" install_binary
|
run_step "安装 CLICD 二进制" install_binary
|
||||||
|
|||||||
Reference in New Issue
Block a user