/* * Copyright (C) 2022-2025. Gardel and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package main import ( "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "errors" "github.com/gin-gonic/gin" "gopkg.in/ini.v1" "gorm.io/gorm" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "yggdrasil-go/model" "yggdrasil-go/router" "yggdrasil-go/service" "yggdrasil-go/util" ) type MetaCfg struct { ServerName string `ini:"server_name"` ImplementationName string `ini:"implementation_name"` ImplementationVersion string `ini:"implementation_version"` SkinDomains []string `ini:"skin_domains"` SkinRootUrl string `ini:"skin_root_url"` } type ServerCfg struct { ServerAddress string `ini:"server_address"` TrustedProxies []string `ini:"trusted_proxies"` } type SmtpCfg struct { SmtpServer string `ini:"smtp_server"` SmtpPort int `ini:"smtp_port"` SmtpSsl bool `ini:"smtp_ssl"` EmailFrom string `ini:"email_from"` SmtpUser string `ini:"smtp_user"` SmtpPassword string `ini:"smtp_password"` TitlePrefix string `ini:"title_prefix"` RegisterTemplate string `ini:"register_template"` ResetPasswordTemplate string `ini:"reset_password_template"` } func main() { configFilePath := "config.ini" cfg, err := ini.LooseLoad(configFilePath) if err != nil { log.Fatal("无法读取配置文件", err) } meta := MetaCfg{ ServerName: "A Mojang Yggdrasil Server", ImplementationName: "go-yggdrasil-server", ImplementationVersion: "v0.0.1", SkinDomains: []string{".example.com", "localhost"}, SkinRootUrl: "http://localhost:8080", } err = cfg.Section("meta").MapTo(&meta) if err != nil { log.Fatal("无法读取配置文件", err) } dbCfg := util.DbCfg{ DatabaseDriver: "sqlite", DatabaseDsn: "file:sqlite.db?cache=shared", } err = cfg.Section("database").MapTo(&dbCfg) if err != nil { log.Fatal("无法读取配置文件", err) } pathSection := cfg.Section("paths") privateKeyPath := pathSection.Key("private_key_file").MustString("private.pem") publicKeyPath := pathSection.Key("public_key_file").MustString("public.pem") serverCfg := ServerCfg{ ServerAddress: ":8080", TrustedProxies: []string{ "127.0.0.0/8", "10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12", }, } err = cfg.Section("server").MapTo(&serverCfg) if err != nil { log.Fatal("无法读取配置文件", err) } smtpCfg := SmtpCfg{ SmtpServer: "localhost", SmtpPort: 25, SmtpSsl: false, EmailFrom: "Go Yggdrasil Server ", SmtpUser: "mc@example.com", SmtpPassword: "123456", TitlePrefix: "[A Mojang Yggdrasil Server]", RegisterTemplate: "请访问下面的链接进行验证:
" + meta.SkinRootUrl + "/profile/#emailVerifyToken={{.AccessToken}}
", ResetPasswordTemplate: "请访问下面的链接进行密码重置:
" + meta.SkinRootUrl + "/profile/resetPassword#passwordResetToken={{.AccessToken}}
", } err = cfg.Section("smtp").MapTo(&smtpCfg) _, err = os.Stat(configFilePath) if err != nil && os.IsNotExist(err) { log.Println("配置文件不存在,已使用默认配置") _ = cfg.Section("meta").ReflectFrom(&meta) _ = cfg.Section("database").ReflectFrom(&dbCfg) _ = cfg.Section("server").ReflectFrom(&serverCfg) _ = cfg.Section("smtp").ReflectFrom(&smtpCfg) err = cfg.SaveToIndent(configFilePath, " ") if err != nil { log.Println("警告: 无法保存配置文件", err) } } checkRsaKeyFile(privateKeyPath, publicKeyPath) publicKeyContent, err := os.ReadFile(publicKeyPath) if err != nil { log.Fatal("无法读取公钥内容", err) } db, err := gorm.Open(util.GetDialector(dbCfg), &gorm.Config{ SkipDefaultTransaction: true, }) if err != nil { log.Fatal("无法连接数据库", err) } err = db.AutoMigrate(&model.User{}, &model.Texture{}) if err != nil { log.Fatal("无法导入数据库", err) } serverMeta := router.ServerMeta{} serverMeta.Meta.ServerName = meta.ServerName serverMeta.Meta.ImplementationName = meta.ImplementationName serverMeta.Meta.ImplementationVersion = meta.ImplementationVersion serverMeta.Meta.FeatureNoMojangNamespace = true serverMeta.Meta.FeatureEnableProfileKey = true serverMeta.Meta.FeatureEnableMojangAntiFeatures = true serverMeta.Meta.Links.Homepage = meta.SkinRootUrl + "/profile/" serverMeta.Meta.Links.Register = meta.SkinRootUrl + "/profile/" serverMeta.SkinDomains = meta.SkinDomains serverMeta.SignaturePublickey = string(publicKeyContent) r := gin.Default() err = r.SetTrustedProxies(serverCfg.TrustedProxies) if err != nil { log.Fatal(err) } smtpConfig := service.SmtpConfig{ SmtpServer: smtpCfg.SmtpServer, SmtpPort: smtpCfg.SmtpPort, SmtpSsl: smtpCfg.SmtpSsl, EmailFrom: smtpCfg.EmailFrom, SmtpUser: smtpCfg.SmtpUser, SmtpPassword: smtpCfg.SmtpPassword, TitlePrefix: smtpCfg.TitlePrefix, RegisterTemplate: smtpCfg.RegisterTemplate, ResetPasswordTemplate: smtpCfg.ResetPasswordTemplate, } router.InitRouters(r, db, &serverMeta, &smtpConfig, meta.SkinRootUrl) r.Static("/profile", "assets") r.NoRoute(func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/profile/") { c.File("assets/index.html") } }) srv := &http.Server{ Addr: serverCfg.ServerAddress, Handler: r, } go func() { if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { log.Printf("listen: %s\n", err) } }() log.Printf("已启动, 地址: %s\n", srv.Addr) quit := make(chan os.Signal) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("关闭...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("强制关闭:", err) } log.Println("退出") } func checkRsaKeyFile(privateKeyPath string, publicKeyPath string) { _, err := os.Stat(privateKeyPath) if err != nil && os.IsNotExist(err) { privatePem, err := os.OpenFile(privateKeyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { log.Fatalln("无法创建私钥文件", err) } defer privatePem.Close() publicPem, err := os.OpenFile(publicKeyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Fatalln("无法创建公钥文件", err) } defer publicPem.Close() privateKey, err := rsa.GenerateKey(rand.Reader, 4096) util.PrivateKey = privateKey if err != nil { log.Fatalln("无法生成 RSA 密钥", err) } privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) if err != nil { log.Fatalln("无法序列化 RSA 密钥", err) } err = pem.Encode(privatePem, &pem.Block{ Type: "PRIVATE KEY", Bytes: privateKeyBytes, }) if err != nil { log.Fatalln("无法写入私钥文件", err) } publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { log.Fatalln("无法序列化 RSA 公钥", err) } err = pem.Encode(publicPem, &pem.Block{ Type: "PUBLIC KEY", Bytes: publicKeyBytes, }) if err != nil { log.Fatalln("无法写入公钥文件", err) } } else if err != nil { log.Fatalln("无法打开私钥文件", err) } else { pemContent, err := os.ReadFile(privateKeyPath) if err != nil { log.Fatalln("无法打开私钥文件", err) } pemBlock, _ := pem.Decode(pemContent) privateKeyI, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes) if err != nil { log.Fatalln("无法解析私钥文件", err) } util.PrivateKey = privateKeyI.(*rsa.PrivateKey) } }