From c3565f72c46a94aaaa2e003c545ab5ab5d632b57 Mon Sep 17 00:00:00 2001 From: zhengkunwang <31820853+zhengkunwang223@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:48:13 +0800 Subject: [PATCH] feat(system-security): Support Hot Reloading of System Certificates (#7152) Refs https://github.com/1Panel-dev/1Panel/issues/7129 --- backend/app/service/website_ca.go | 3 + backend/app/service/website_ssl.go | 28 ++++++++ backend/app/service/website_utils.go | 28 ++------ backend/constant/common.go | 4 ++ backend/cron/job/ssl.go | 21 ------ backend/i18n/lang/en.yaml | 2 + backend/i18n/lang/zh-Hant.yaml | 2 + backend/i18n/lang/zh.yaml | 2 + backend/server/server.go | 12 ++-- frontend/src/api/interface/setting.ts | 1 - frontend/src/lang/modules/en.ts | 5 -- frontend/src/lang/modules/tw.ts | 3 - frontend/src/lang/modules/zh.ts | 3 - frontend/src/routers/modules/setting.ts | 12 ++-- frontend/src/views/setting/safe/index.vue | 3 - frontend/src/views/setting/safe/ssl/index.vue | 71 +++++++------------ go.mod | 2 +- go.sum | 4 +- 18 files changed, 89 insertions(+), 117 deletions(-) diff --git a/backend/app/service/website_ca.go b/backend/app/service/website_ca.go index 3a52dbbd5..6ba841fe9 100644 --- a/backend/app/service/website_ca.go +++ b/backend/app/service/website_ca.go @@ -381,6 +381,9 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website logger.Println(i18n.GetMsgByKey("ExecShellSuccess")) } } + + reloadSystemSSL(websiteSSL, logger) + return websiteSSL, nil } diff --git a/backend/app/service/website_ssl.go b/backend/app/service/website_ssl.go index 0cd752770..91eb47167 100644 --- a/backend/app/service/website_ssl.go +++ b/backend/app/service/website_ssl.go @@ -3,6 +3,7 @@ package service import ( "context" "crypto" + "crypto/tls" "crypto/x509" "encoding/pem" "fmt" @@ -188,6 +189,31 @@ func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{ logger.Println(i18n.GetMsgWithMap(msgKey, params)) } +func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) { + systemSSLEnable, sslID := GetSystemSSL() + if systemSSLEnable && sslID == websiteSSL.ID { + fileOp := files.NewFileOp() + certPath := path.Join(global.CONF.System.BaseDir, "1panel/secret/server.crt") + keyPath := path.Join(global.CONF.System.BaseDir, "1panel/secret/server.key") + printSSLLog(logger, "StartUpdateSystemSSL", nil, logger == nil) + if err := fileOp.WriteFile(certPath, strings.NewReader(websiteSSL.Pem), 0600); err != nil { + logger.Printf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) + return + } + if err := fileOp.WriteFile(keyPath, strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { + logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) + return + } + newCert, err := tls.X509KeyPair([]byte(websiteSSL.Pem), []byte(websiteSSL.PrivateKey)) + if err != nil { + logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) + return + } + printSSLLog(logger, "UpdateSystemSSLSuccess", nil, logger == nil) + constant.CertStore.Store(&newCert) + } +} + func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { var ( err error @@ -344,6 +370,8 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error { } printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil, apply.DisableLog) } + + reloadSystemSSL(websiteSSL, logger) }() return nil diff --git a/backend/app/service/website_utils.go b/backend/app/service/website_utils.go index e6380678a..88e256273 100644 --- a/backend/app/service/website_utils.go +++ b/backend/app/service/website_utils.go @@ -1002,23 +1002,22 @@ func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) { } } -func GetSystemSSL() (bool, bool, uint) { +func GetSystemSSL() (bool, uint) { sslSetting, err := settingRepo.Get(settingRepo.WithByKey("SSL")) if err != nil { global.LOG.Errorf("load service ssl from setting failed, err: %v", err) - return false, false, 0 + return false, 0 } if sslSetting.Value == "enable" { sslID, _ := settingRepo.Get(settingRepo.WithByKey("SSLID")) idValue, _ := strconv.Atoi(sslID.Value) if idValue <= 0 { - return false, false, 0 + return false, 0 } - auto, _ := settingRepo.Get(settingRepo.WithByKey("AutoRestart")) - return true, auto.Value == "enable", uint(idValue) + return true, uint(idValue) } - return false, false, 0 + return false, 0 } func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error { @@ -1037,22 +1036,7 @@ func UpdateSSLConfig(websiteSSL model.WebsiteSSL) error { return buserr.WithErr(constant.ErrSSLApply, err) } } - enable, auto, sslID := GetSystemSSL() - if enable && sslID == websiteSSL.ID { - fileOp := files.NewFileOp() - secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret") - if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) - return err - } - if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error()) - return err - } - if auto { - _, _ = cmd.Exec("systemctl restart 1panel.service") - } - } + reloadSystemSSL(&websiteSSL, nil) return nil } diff --git a/backend/constant/common.go b/backend/constant/common.go index 0864b633d..f9f0b33aa 100644 --- a/backend/constant/common.go +++ b/backend/constant/common.go @@ -1,5 +1,7 @@ package constant +import "sync/atomic" + type DBContext string const ( @@ -123,3 +125,5 @@ var DynamicRoutes = []string{ `^/databases/postgresql/setting/[^/]+/[^/]+$`, `^/websites/[^/]+/config/[^/]+$`, } + +var CertStore atomic.Value diff --git a/backend/cron/job/ssl.go b/backend/cron/job/ssl.go index f6f906516..685333dd9 100644 --- a/backend/cron/job/ssl.go +++ b/backend/cron/job/ssl.go @@ -1,8 +1,6 @@ package job import ( - "path" - "strings" "time" "github.com/1Panel-dev/1Panel/backend/app/dto/request" @@ -10,9 +8,7 @@ import ( "github.com/1Panel-dev/1Panel/backend/app/service" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" - "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/common" - "github.com/1Panel-dev/1Panel/backend/utils/files" ) type ssl struct { @@ -23,7 +19,6 @@ func NewSSLJob() *ssl { } func (ssl *ssl) Run() { - systemSSLEnable, auto, sslID := service.GetSystemSSL() sslRepo := repo.NewISSLRepo() sslService := service.NewIWebsiteSSLService() sslList, _ := sslRepo.List() @@ -59,22 +54,6 @@ func (ssl *ssl) Run() { continue } } - if systemSSLEnable && sslID == s.ID { - websiteSSL, _ := sslRepo.GetFirst(repo.NewCommonRepo().WithByID(s.ID)) - fileOp := files.NewFileOp() - secretDir := path.Join(global.CONF.System.BaseDir, "1panel/secret") - if err := fileOp.WriteFile(path.Join(secretDir, "server.crt"), strings.NewReader(websiteSSL.Pem), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error()) - continue - } - if err := fileOp.WriteFile(path.Join(secretDir, "server.key"), strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil { - global.LOG.Errorf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", s.PrimaryDomain, err.Error()) - continue - } - if auto { - _, _ = cmd.Exec("systemctl restart 1panel.service") - } - } global.LOG.Infof("The SSL certificate for the [%s] domain has been successfully updated", s.PrimaryDomain) } } diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index b62076f3b..9e123a8a7 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -127,6 +127,8 @@ ErrDefaultCA: "The default organization cannot be deleted" ApplyWebSiteSSLLog: "Start updating {{ .name }} website certificate" ErrUpdateWebsiteSSL: "{{ .name }} website failed to update certificate: {{ .err }}" ApplyWebSiteSSLSuccess: "Update website certificate successfully" +StartUpdateSystemSSL: "Start updating system certificate" +UpdateSystemSSLSuccess: "Update system certificate successfully" #mysql ErrUserIsExist: "The current user already exists. Please enter a new user" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index c5f10ead6..6b42011e0 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -126,6 +126,8 @@ ErrDefaultCA: "默認機構不能刪除" ApplyWebSiteSSLLog: "開始更新 {{ .name }} 網站憑證" ErrUpdateWebsiteSSL: "{{ .name }} 網站更新憑證失敗: {{ .err }}" ApplyWebSiteSSLSuccess: "更新網站憑證成功" +StartUpdateSystemSSL: "開始更新系統證書" +UpdateSystemSSLSuccess: "更新系統證書成功" #mysql diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 4880df5f6..145c7e587 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -130,6 +130,8 @@ ApplyWebSiteSSLSuccess: "更新网站证书成功" ErrExecShell: "执行脚本失败 {{ .err }}" ExecShellStart: "开始执行脚本" ExecShellSuccess: "脚本执行成功" +StartUpdateSystemSSL: "开始更新系统证书" +UpdateSystemSSLSuccess: "更新系统证书成功" #mysql ErrUserIsExist: "当前用户已存在,请重新输入" diff --git a/backend/server/server.go b/backend/server/server.go index a8d468690..eb5a4973b 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -4,13 +4,13 @@ import ( "crypto/tls" "encoding/gob" "fmt" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/i18n" "net" "net/http" "os" "path" - "github.com/1Panel-dev/1Panel/backend/i18n" - "github.com/1Panel-dev/1Panel/backend/init/app" "github.com/1Panel-dev/1Panel/backend/init/business" @@ -81,12 +81,16 @@ func Start() { if err != nil { panic(err) } + constant.CertStore.Store(&cert) + server.TLSConfig = &tls.Config{ - Certificates: []tls.Certificate{cert}, + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + return constant.CertStore.Load().(*tls.Certificate), nil + }, } global.LOG.Infof("listen at https://%s:%s [%s]", global.CONF.System.BindAddress, global.CONF.System.Port, tcpItem) - if err := server.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, certPath, keyPath); err != nil { + if err := server.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, "", ""); err != nil { panic(err) } } else { diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index d908d586a..88ad99539 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -29,7 +29,6 @@ export namespace Setting { bindAddress: string; ssl: string; sslType: string; - autoRestart: string; allowIPs: string; bindDomain: string; securityEntrance: string; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 56064f84c..2de099497 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1551,11 +1551,6 @@ const message = { bindDomain: 'Bind Domain', unBindDomain: 'Unbind domain', panelSSL: 'Panel SSL', - sslAutoRestart: 'Restart 1Panel service after certificate auto-renewal', - sslChangeHelper1: - 'Currently, automatic restart of 1Panel service is not selected. The certificate auto-renewal will not take effect immediately and will still require a manual restart of 1Panel.', - sslChangeHelper2: - 'The 1Panel service will automatically restart after setting the panel SSL. Do you want to continue?', unBindDomainHelper: 'The action of unbinding a domain name may cause system insecurity. Do you want to continue?', bindDomainHelper: diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 484e6b216..71fe21f8d 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1494,9 +1494,6 @@ const message = { bindDomain: '域名綁定', unBindDomain: '域名解綁', panelSSL: '面板 SSL', - sslAutoRestart: '證書自動續期後重啟 1Panel 服務', - sslChangeHelper1: '當前未勾選自動重啟 1Panel 服務,證書自動續期後不會立即生效,仍需手動重啟 1Panel。', - sslChangeHelper2: '設置面板 SSL 後將自動重啟 1Panel 服務,是否繼續?', unBindDomainHelper: '解除域名綁定可能造成系統不安全,是否繼續?', bindDomainHelper: '設置域名綁定後,僅能通過設置中域名訪問 1Panel 服務', bindDomainHelper1: '綁定域名為空時,則取消域名綁定', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 6e7b7c3cd..d8538ddf0 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1496,9 +1496,6 @@ const message = { bindDomain: '域名绑定', unBindDomain: '域名解绑', panelSSL: '面板 SSL', - sslAutoRestart: '证书自动续期后重启 1Panel 服务', - sslChangeHelper1: '当前未勾选自动重启 1Panel 服务,证书自动续期后不会立即生效,仍需手动重启 1Panel。', - sslChangeHelper2: '设置面板 SSL 后将自动重启 1Panel 服务,是否继续?', unBindDomainHelper: '解除域名绑定可能造成系统不安全,是否继续?', bindDomainHelper: '设置域名绑定后,仅能通过设置中域名访问 1Panel 服务', bindDomainHelper1: '绑定域名为空时,则取消域名绑定', diff --git a/frontend/src/routers/modules/setting.ts b/frontend/src/routers/modules/setting.ts index 99e6def22..d0f8984f2 100644 --- a/frontend/src/routers/modules/setting.ts +++ b/frontend/src/routers/modules/setting.ts @@ -34,7 +34,7 @@ const settingRouter = { hidden: true, meta: { requiresAuth: true, - activeMenu: 'Setting', + activeMenu: '/settings', }, }, { @@ -44,7 +44,7 @@ const settingRouter = { hidden: true, meta: { requiresAuth: true, - activeMenu: 'Setting', + activeMenu: '/settings', }, }, { @@ -54,7 +54,7 @@ const settingRouter = { hidden: true, meta: { requiresAuth: true, - activeMenu: 'Setting', + activeMenu: '/settings', }, }, { @@ -64,7 +64,7 @@ const settingRouter = { hidden: true, meta: { requiresAuth: true, - activeMenu: 'Setting', + activeMenu: '/settings', }, }, { @@ -74,7 +74,7 @@ const settingRouter = { component: () => import('@/views/setting/snapshot/index.vue'), meta: { requiresAuth: true, - activeMenu: 'Setting', + activeMenu: '/settings', }, }, { @@ -84,7 +84,7 @@ const settingRouter = { component: () => import('@/views/setting/expired.vue'), meta: { requiresAuth: true, - activeMenu: 'Expired', + activeMenu: '/settings', }, }, ], diff --git a/frontend/src/views/setting/safe/index.vue b/frontend/src/views/setting/safe/index.vue index a9c959ff7..b37f66116 100644 --- a/frontend/src/views/setting/safe/index.vue +++ b/frontend/src/views/setting/safe/index.vue @@ -225,7 +225,6 @@ const form = reactive({ bindAddress: '', ssl: 'disable', sslType: 'self', - autoRestart: 'disable', securityEntrance: '', expirationDays: 0, expirationTime: '', @@ -250,7 +249,6 @@ const search = async () => { if (form.ssl === 'enable') { loadInfo(); } - form.autoRestart = res.data.autoRestart; form.securityEntrance = res.data.securityEntrance; form.expirationDays = Number(res.data.expirationDays); form.expirationTime = res.data.expirationTime; @@ -330,7 +328,6 @@ const handleSSL = async () => { ssl: form.ssl, sslType: form.sslType, sslInfo: sslInfo.value, - autoRestart: form.autoRestart, }; sslRef.value!.acceptParams(params); return; diff --git a/frontend/src/views/setting/safe/ssl/index.vue b/frontend/src/views/setting/safe/ssl/index.vue index c4c9119d8..5ae485d4e 100644 --- a/frontend/src/views/setting/safe/ssl/index.vue +++ b/frontend/src/views/setting/safe/ssl/index.vue @@ -112,14 +112,6 @@ - - - {{ $t('setting.sslAutoRestart') }} - - - {{ $t('setting.sslChangeHelper1') }} - - @@ -143,7 +135,7 @@ import i18n from '@/lang'; import { MsgSuccess } from '@/utils/message'; import { downloadSSL, updateSSL } from '@/api/modules/setting'; import { Rules } from '@/global/form-rules'; -import { ElMessageBox, FormInstance } from 'element-plus'; +import { FormInstance } from 'element-plus'; import { Setting } from '@/api/interface/setting'; import DrawerHeader from '@/components/drawer-header/index.vue'; import { GlobalStore } from '@/store'; @@ -162,7 +154,6 @@ const form = reactive({ key: '', rootPath: '', timeout: '', - autoRestart: 'disable', }); const rules = reactive({ @@ -179,7 +170,6 @@ const itemSSL = ref(); interface DialogProps { sslType: string; sslInfo?: Setting.SSLInfo; - autoRestart: string; } const acceptParams = async (params: DialogProps): Promise => { if (params.sslType.indexOf('-') !== -1) { @@ -202,7 +192,6 @@ const acceptParams = async (params: DialogProps): Promise => { } else { loadSSLs(); } - form.autoRestart = params.autoRestart; drawerVisible.value = true; }; const emit = defineEmits<{ (e: 'search'): void }>(); @@ -243,41 +232,31 @@ const onSaveSSL = async (formEl: FormInstance | undefined) => { if (!formEl) return; formEl.validate(async (valid) => { if (!valid) return; - let msg = !form.autoRestart - ? i18n.global.t('setting.sslChangeHelper1') + '\n\n\n' + 'qwdqwdqwd' - : i18n.global.t('setting.sslChangeHelper2'); - ElMessageBox.confirm(msg, i18n.global.t('setting.panelSSL'), { - confirmButtonText: i18n.global.t('commons.button.confirm'), - cancelButtonText: i18n.global.t('commons.button.cancel'), - type: 'info', - }).then(async () => { - let itemType = form.sslType; - if (form.sslType === 'import') { - itemType = form.itemSSLType === 'paste' ? 'import-paste' : 'import-local'; - } - let param = { - ssl: 'enable', - sslType: itemType, - domain: '', - sslID: form.sslID, - cert: form.cert, - key: form.key, - autoRestart: form.autoRestart, - }; + let itemType = form.sslType; + if (form.sslType === 'import') { + itemType = form.itemSSLType === 'paste' ? 'import-paste' : 'import-local'; + } + let param = { + ssl: 'enable', + sslType: itemType, + domain: '', + sslID: form.sslID, + cert: form.cert, + key: form.key, + }; + let href = window.location.href; + param.domain = href.split('//')[1].split(':')[0]; + await updateSSL(param).then(() => { + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); let href = window.location.href; - param.domain = href.split('//')[1].split(':')[0]; - await updateSSL(param).then(() => { - MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); - let href = window.location.href; - globalStore.isLogin = false; - let address = href.split('://')[1]; - if (globalStore.entrance) { - address = address.replaceAll('settings/safe', globalStore.entrance); - } else { - address = address.replaceAll('settings/safe', 'login'); - } - window.open(`https://${address}`, '_self'); - }); + globalStore.isLogin = false; + let address = href.split('://')[1]; + if (globalStore.entrance) { + address = address.replaceAll('settings/safe', globalStore.entrance); + } else { + address = address.replaceAll('settings/safe', 'login'); + } + window.open(`https://${address}`, '_self'); }); }); }; diff --git a/go.mod b/go.mod index 59bcc4a92..b41be18ae 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/gin-contrib/gzip v0.0.6 github.com/gin-gonic/gin v1.9.1 github.com/glebarez/sqlite v1.10.0 - github.com/go-acme/lego/v4 v4.20.2 + github.com/go-acme/lego/v4 v4.20.4 github.com/go-gormigrate/gormigrate/v2 v2.1.1 github.com/go-playground/validator/v10 v10.18.0 github.com/go-redis/redis v6.15.9+incompatible diff --git a/go.sum b/go.sum index 38bd1cd3f..b3703193a 100644 --- a/go.sum +++ b/go.sum @@ -318,8 +318,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.10.0 h1:u4gt8y7OND/cCei/NMHmfbLxF6xP2wgKcT/BJf2pYkc= github.com/glebarez/sqlite v1.10.0/go.mod h1:IJ+lfSOmiekhQsFTJRx/lHtGYmCdtAiTaf5wI9u5uHA= -github.com/go-acme/lego/v4 v4.20.2 h1:ZwO3oLZb8fL6up1OZVJP3yHuvqhozzlEmyqKmhrPchQ= -github.com/go-acme/lego/v4 v4.20.2/go.mod h1:foauPlhnhoq8WUphaWx5U04uDc+JGhk4ZZtPz/Vqsjg= +github.com/go-acme/lego/v4 v4.20.4 h1:yCQGBX9jOfMbriEQUocdYm7EBapdTp8nLXYG8k6SqSU= +github.com/go-acme/lego/v4 v4.20.4/go.mod h1:foauPlhnhoq8WUphaWx5U04uDc+JGhk4ZZtPz/Vqsjg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=