192 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			192 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> 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 <https://www.gnu.org/licenses/>.
 | 
						|
 */
 | 
						|
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/rand"
 | 
						|
	"crypto/rsa"
 | 
						|
	"crypto/x509"
 | 
						|
	"embed"
 | 
						|
	"encoding/pem"
 | 
						|
	"errors"
 | 
						|
	"github.com/gin-gonic/gin"
 | 
						|
	"gopkg.in/ini.v1"
 | 
						|
	"gorm.io/driver/sqlite"
 | 
						|
	"gorm.io/gorm"
 | 
						|
	"io/fs"
 | 
						|
	"io/ioutil"
 | 
						|
	"log"
 | 
						|
	"net/http"
 | 
						|
	"os"
 | 
						|
	"os/signal"
 | 
						|
	"syscall"
 | 
						|
	"time"
 | 
						|
	"yggdrasil-go/model"
 | 
						|
	"yggdrasil-go/router"
 | 
						|
	"yggdrasil-go/util"
 | 
						|
)
 | 
						|
 | 
						|
//go:embed assets/*
 | 
						|
var f embed.FS
 | 
						|
 | 
						|
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"`
 | 
						|
}
 | 
						|
 | 
						|
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.1",
 | 
						|
		SkinDomains:           []string{".example.com", "localhost"},
 | 
						|
		SkinRootUrl:           "http://localhost:8080",
 | 
						|
	}
 | 
						|
	err = cfg.Section("meta").MapTo(&meta)
 | 
						|
	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")
 | 
						|
	databasePath := pathSection.Key("database_file").MustString("sqlite.db")
 | 
						|
	address := cfg.Section("server").Key("server_address").MustString(":8080")
 | 
						|
	_, err = os.Stat(configFilePath)
 | 
						|
	if err != nil && os.IsNotExist(err) {
 | 
						|
		log.Println("配置文件不存在,已使用默认配置")
 | 
						|
		_ = cfg.Section("meta").ReflectFrom(&meta)
 | 
						|
		err = cfg.SaveToIndent(configFilePath, " ")
 | 
						|
		if err != nil {
 | 
						|
			log.Println("警告: 无法保存配置文件", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	checkRsaKeyFile(privateKeyPath, publicKeyPath)
 | 
						|
	publicKeyContent, err := ioutil.ReadFile(publicKeyPath)
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal("无法读取公钥内容", err)
 | 
						|
	}
 | 
						|
	db, err := gorm.Open(sqlite.Open("file:"+databasePath+"?cache=shared"), &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.Links.Homepage = meta.SkinRootUrl + "/profile/user.html"
 | 
						|
	serverMeta.Meta.Links.Register = meta.SkinRootUrl + "/profile/index.html"
 | 
						|
	serverMeta.SkinDomains = meta.SkinDomains
 | 
						|
	serverMeta.SignaturePublickey = string(publicKeyContent)
 | 
						|
	r := gin.Default()
 | 
						|
	router.InitRouters(r, db, &serverMeta, meta.SkinRootUrl)
 | 
						|
	assetsFs, err := fs.Sub(f, "assets")
 | 
						|
	if err != nil {
 | 
						|
		log.Fatal(err)
 | 
						|
	}
 | 
						|
	r.StaticFS("/profile", http.FS(assetsFs))
 | 
						|
	srv := &http.Server{
 | 
						|
		Addr:    address,
 | 
						|
		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",
 | 
						|
			Bytes: privateKeyBytes,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			log.Fatalln("无法写入私钥文件", err)
 | 
						|
		}
 | 
						|
		publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
 | 
						|
		err = pem.Encode(publicPem, &pem.Block{
 | 
						|
			Type:  "PUBLIC",
 | 
						|
			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)
 | 
						|
	}
 | 
						|
}
 |