cdrop — 跨 OS 剪贴板与文件传输服务
Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。 - 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。 - 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。 - 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。 - 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。 架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
//go:build darwin
|
||||
|
||||
// clipprobe 是一个只读诊断工具:监听本机剪贴板变化,打印每次变化时
|
||||
// NSPasteboard 上挂的全部类型(UTI)。它不读取也不上传任何剪贴板内容,
|
||||
// 只看「类型」,用于在真机上经验性地确定各应用(尤其密码管理器)复制时
|
||||
// 打哪些 UTI,从而让 D4 的敏感内容过滤基于实测数据而非猜测。
|
||||
//
|
||||
// 用法:go run ./cmd/clipprobe(或 go build 后运行二进制),然后依次从
|
||||
// 普通文本编辑器 / 1Password / Bitwarden / 浏览器密码自动填充 等复制,观察
|
||||
// 各自打印的 UTI。含 org.nspasteboard.ConcealedType / TransientType /
|
||||
// AutoGeneratedType 或厂商私有 UTI(如 com.agilebits.onepassword)者,即为
|
||||
// D4 应跳过上传的敏感内容。
|
||||
//
|
||||
// 实现用 ebitengine/purego + objc 运行时直调 AppKit,无 cgo、可随 Wails
|
||||
// 交叉编译;调用模式照搬 golang.design/x/clipboard 的 darwin 实现。
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ebitengine/purego"
|
||||
"github.com/ebitengine/purego/objc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// objc 调用必须钉在同一 OS 线程上(autorelease pool 的语义要求)。
|
||||
runtime.LockOSThread()
|
||||
|
||||
// 加载 AppKit / Foundation,使 NSPasteboard / NSAutoreleasePool 等类可解析。
|
||||
for _, fw := range []string{
|
||||
"/System/Library/Frameworks/AppKit.framework/AppKit",
|
||||
"/System/Library/Frameworks/Foundation.framework/Foundation",
|
||||
} {
|
||||
if _, err := purego.Dlopen(fw, purego.RTLD_NOW|purego.RTLD_GLOBAL); err != nil {
|
||||
panic(fmt.Sprintf("clipprobe: 无法加载 %s: %v", fw, err))
|
||||
}
|
||||
}
|
||||
|
||||
pbClass := objc.GetClass("NSPasteboard")
|
||||
poolClass := objc.GetClass("NSAutoreleasePool")
|
||||
var (
|
||||
selGeneral = objc.RegisterName("generalPasteboard")
|
||||
selChangeCount = objc.RegisterName("changeCount")
|
||||
selTypes = objc.RegisterName("types")
|
||||
selCount = objc.RegisterName("count")
|
||||
selObjectAt = objc.RegisterName("objectAtIndex:")
|
||||
selUTF8 = objc.RegisterName("UTF8String")
|
||||
selAlloc = objc.RegisterName("alloc")
|
||||
selInit = objc.RegisterName("init")
|
||||
selRelease = objc.RegisterName("release")
|
||||
)
|
||||
|
||||
fmt.Println("clipprobe:监听剪贴板,打印每次变化的 pasteboard 类型(UTI)。Ctrl+C 退出。")
|
||||
fmt.Println("依次从 普通文本 / 1Password / Bitwarden / 浏览器密码自动填充 复制,观察各自的 UTI。")
|
||||
fmt.Println("含 org.nspasteboard.ConcealedType / TransientType / AutoGeneratedType")
|
||||
fmt.Println("或厂商私有 UTI(com.agilebits.onepassword 等)者 = 应跳过上传的敏感内容。")
|
||||
fmt.Println("(本工具只读类型、不读内容、不上传。)")
|
||||
fmt.Println("--------")
|
||||
|
||||
last := -1
|
||||
for {
|
||||
pool := objc.ID(poolClass).Send(selAlloc).Send(selInit)
|
||||
|
||||
general := objc.ID(pbClass).Send(selGeneral)
|
||||
cc := int(general.Send(selChangeCount))
|
||||
if cc != last {
|
||||
last = cc
|
||||
types := general.Send(selTypes)
|
||||
n := int(types.Send(selCount))
|
||||
fmt.Printf("[changeCount=%d] %d 个类型:\n", cc, n)
|
||||
for i := 0; i < n; i++ {
|
||||
item := types.Send(selObjectAt, i)
|
||||
cstr := item.Send(selUTF8)
|
||||
fmt.Printf(" - %s\n", cString(uintptr(cstr)))
|
||||
}
|
||||
}
|
||||
|
||||
objc.ID(pool).Send(selRelease)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// cString reads a NUL-terminated C string (from -[NSString UTF8String]) into a
|
||||
// Go string.
|
||||
func cString(p uintptr) string {
|
||||
if p == 0 {
|
||||
return "(nil)"
|
||||
}
|
||||
var b []byte
|
||||
for i := 0; ; i++ {
|
||||
c := *(*byte)(unsafe.Pointer(p + uintptr(i)))
|
||||
if c == 0 {
|
||||
break
|
||||
}
|
||||
b = append(b, c)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user