feat: 授权延长——同事一键申请,管理员批准时决定时长;时长支持 30 分钟与自定义
同事端 - 用过邀请链接的设备再次打开链接时显示自己的授权状态:有效时可申请延长, 访问结束(到期或通道关闭)后 2 小时内也可以;超过 2 小时或被撤销则提示重新获取邀请 - 「申请延长」只有一个按钮,提交后复用审批状态页,批准后显示新的到期时间 - 用过链接的设备不再自动重新放通,统一走申请延长 网关 - 授权结束后保留 2 小时的记录(通道关闭后仍保留,随状态文件持久化);撤销的不能延长, 删除通道 / 解散组时一并清除(unregister 带 deleted 标记) - 新增 extend_lease:有效授权从原到期时间顺延,已结束的从现在起算,不受通道最长开启时长截断, 并批准该设备的延长申请;指标里带上全部可延长设备与申请(包括已关闭的通道) - 授权时长支持分钟数("30m"、"90m",5 分钟到 30 天) Agent / App / 网页面板 - POST .../leases/extend:通道已关闭时先自动开启(组则全组开启),并让通道至少开到授权结束 - 授权管理新增「申请延长」「最近结束 · 2 小时内可延长」,已授权设备可直接「延长」; 卡片、菜单栏和系统通知提示延长申请,菜单栏可直接选时长批准 - 邀请的可访问时长、通道最长开启时长增加 30 分钟与自定义;网页面板弹窗层级高于侧边抽屉 - 邀请分享文案提示「授权结束后 2 小时内可重新打开此链接申请延长」
This commit is contained in:
1 parent
9a675ba96e
commit
dceb59c0ca
16 files changed
+1707
-75
No files matched your search
+70
-2
@@ -34,6 +34,7 @@ type Agent struct {
|
||||
latestMetrics protocol.MetricsMessage
|
||||
regErrors map[string]string // tunnelID -> 服务器返回的注册失败原因
|
||||
inviteWaiters map[string]chan protocol.InviteRespMessage
|
||||
extendWaiters map[string]chan protocol.ExtendLeaseRespMessage
|
||||
|
||||
controlWriteMu sync.Mutex
|
||||
controlWriter *bufio.Writer
|
||||
@@ -57,6 +58,7 @@ func NewAgent(cm *ConfigManager) *Agent {
|
||||
cm: cm,
|
||||
regErrors: make(map[string]string),
|
||||
inviteWaiters: make(map[string]chan protocol.InviteRespMessage),
|
||||
extendWaiters: make(map[string]chan protocol.ExtendLeaseRespMessage),
|
||||
usage: make(map[string]*usageState),
|
||||
idleUnit: time.Minute,
|
||||
rates: newRateRecorder(),
|
||||
@@ -199,6 +201,18 @@ func (a *Agent) trackUsage(now time.Time) {
|
||||
groupBusy[gm.GroupID] = true
|
||||
}
|
||||
}
|
||||
// 等待批准的延长申请:通道(组)保持开启,避免关了又被批准重开
|
||||
renewing := make(map[string]bool)
|
||||
for _, r := range metrics.Renewals {
|
||||
if r.Request == nil {
|
||||
continue
|
||||
}
|
||||
if r.GroupID != "" {
|
||||
groupBusy[r.GroupID] = true
|
||||
} else {
|
||||
renewing[r.TunnelID] = true
|
||||
}
|
||||
}
|
||||
for _, t := range cfg.Tunnels {
|
||||
if m, ok := byID[t.ID]; ok && t.Enabled && t.GroupID != "" && m.Status == "online" && tunnelInUse(m) {
|
||||
groupBusy[t.GroupID] = true
|
||||
@@ -242,7 +256,7 @@ func (a *Agent) trackUsage(now time.Time) {
|
||||
firstConn = append(firstConn, t.ID)
|
||||
}
|
||||
}
|
||||
u.inUse = tunnelInUse(m) || (t.GroupID != "" && groupBusy[t.GroupID])
|
||||
u.inUse = tunnelInUse(m) || renewing[t.ID] || (t.GroupID != "" && groupBusy[t.GroupID])
|
||||
if u.inUse {
|
||||
u.lastActive = now
|
||||
}
|
||||
@@ -520,6 +534,17 @@ func (a *Agent) connectAndRun(cfg Config) error {
|
||||
delete(a.inviteWaiters, resp.RequestID)
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case protocol.TypeExtendResp:
|
||||
var resp protocol.ExtendLeaseRespMessage
|
||||
if json.Unmarshal(line, &resp) != nil {
|
||||
continue
|
||||
}
|
||||
a.mu.Lock()
|
||||
if ch, ok := a.extendWaiters[resp.RequestID]; ok {
|
||||
ch <- resp
|
||||
delete(a.extendWaiters, resp.RequestID)
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case protocol.TypeRegisterResp:
|
||||
var resp protocol.RegisterRespMessage
|
||||
if json.Unmarshal(line, &resp) != nil {
|
||||
@@ -743,14 +768,24 @@ func (a *Agent) wire(t protocol.TunnelConfig) protocol.TunnelConfig {
|
||||
return t
|
||||
}
|
||||
|
||||
// RemoveTunnel closes a tunnel on the relay server; its devices can still ask for an extension
|
||||
func (a *Agent) RemoveTunnel(id string) {
|
||||
a.unregister(id, false)
|
||||
}
|
||||
|
||||
// DeleteTunnel removes a deleted tunnel from the relay server, renewal records included
|
||||
func (a *Agent) DeleteTunnel(id string) {
|
||||
a.unregister(id, true)
|
||||
}
|
||||
|
||||
func (a *Agent) unregister(id string, deleted bool) {
|
||||
a.mu.Lock()
|
||||
delete(a.regErrors, id)
|
||||
a.mu.Unlock()
|
||||
if !a.IsConnected() {
|
||||
return
|
||||
}
|
||||
_ = a.sendControl(protocol.UnregisterMessage{Type: protocol.TypeUnregister, TunnelID: id})
|
||||
_ = a.sendControl(protocol.UnregisterMessage{Type: protocol.TypeUnregister, TunnelID: id, Deleted: deleted})
|
||||
}
|
||||
|
||||
// Scope selects what an invite, request or lease applies to: one tunnel, or a whole group
|
||||
@@ -792,6 +827,38 @@ func (a *Agent) CreateInvite(scope Scope, note string, validMinutes, maxUses int
|
||||
}
|
||||
}
|
||||
|
||||
// ExtendLease extends a device's access by minutes (approving its renewal requests) and returns
|
||||
// the new expiry
|
||||
func (a *Agent) ExtendLease(scope Scope, ip string, minutes int) (time.Time, error) {
|
||||
if !a.IsConnected() {
|
||||
return time.Time{}, fmt.Errorf("未连接中继服务器")
|
||||
}
|
||||
reqID := uuid.New().String()
|
||||
ch := make(chan protocol.ExtendLeaseRespMessage, 1)
|
||||
a.mu.Lock()
|
||||
a.extendWaiters[reqID] = ch
|
||||
a.mu.Unlock()
|
||||
defer func() {
|
||||
a.mu.Lock()
|
||||
delete(a.extendWaiters, reqID)
|
||||
a.mu.Unlock()
|
||||
}()
|
||||
if err := a.sendControl(protocol.ExtendLeaseMessage{
|
||||
Type: protocol.TypeExtendLease, RequestID: reqID, TunnelID: scope.TunnelID, GroupID: scope.GroupID, IP: ip, Minutes: minutes,
|
||||
}); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
select {
|
||||
case resp := <-ch:
|
||||
if !resp.Success {
|
||||
return time.Time{}, fmt.Errorf("%s", resp.Error)
|
||||
}
|
||||
return resp.ExpiresAt, nil
|
||||
case <-time.After(5 * time.Second):
|
||||
return time.Time{}, fmt.Errorf("服务器未响应,请先把服务端更新到支持延长授权的版本")
|
||||
}
|
||||
}
|
||||
|
||||
// RevokeInvite invalidates an invite link and the leases it granted
|
||||
func (a *Agent) RevokeInvite(scope Scope, code string) error {
|
||||
if !a.IsConnected() {
|
||||
@@ -859,5 +926,6 @@ func (a *Agent) GetLatestMetrics() protocol.MetricsMessage {
|
||||
m.Groups = append([]protocol.GroupMetrics(nil), m.Groups...)
|
||||
m.ActiveConns = append([]protocol.ActiveConnInfo(nil), m.ActiveConns...)
|
||||
m.SecurityEvents = append([]protocol.SecurityEvent(nil), m.SecurityEvents...)
|
||||
m.Renewals = append([]protocol.RenewalInfo(nil), m.Renewals...)
|
||||
return m
|
||||
}
|
||||
Reference in new issue
Block a user