perf: Install certificates and optimize proxy settings

This commit is contained in:
putyy
2025-05-09 17:56:11 +08:00
parent 29cc879b85
commit 7793f83ea3
24 changed files with 381 additions and 193 deletions

72
core/aes.go Normal file
View File

@@ -0,0 +1,72 @@
package core
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
)
type AESCipher struct {
key []byte
}
func NewAESCipher(key string) *AESCipher {
return &AESCipher{key: []byte(key)}
}
func (a *AESCipher) Encrypt(plainText string) (string, error) {
block, err := aes.NewCipher(a.key)
if err != nil {
return "", err
}
padding := block.BlockSize() - len(plainText)%block.BlockSize()
padText := bytes.Repeat([]byte{byte(padding)}, padding)
plainText = plainText + string(padText)
cipherText := make([]byte, aes.BlockSize+len(plainText))
iv := cipherText[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText[aes.BlockSize:], []byte(plainText))
return base64.StdEncoding.EncodeToString(cipherText), nil
}
func (a *AESCipher) Decrypt(cipherText string) (string, error) {
cipherTextBytes, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
block, err := aes.NewCipher(a.key)
if err != nil {
return "", err
}
if len(cipherTextBytes) < aes.BlockSize {
return "", errors.New("ciphertext too short")
}
iv := cipherTextBytes[:aes.BlockSize]
cipherTextBytes = cipherTextBytes[aes.BlockSize:]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(cipherTextBytes, cipherTextBytes)
padding := int(cipherTextBytes[len(cipherTextBytes)-1])
if padding > len(cipherTextBytes) || padding > aes.BlockSize {
return "", errors.New("padding size error")
}
plainText := cipherTextBytes[:len(cipherTextBytes)-padding]
return string(plainText), nil
}

View File

@@ -3,14 +3,12 @@ package core
import (
"context"
"embed"
"fmt"
"github.com/vrischmann/userdir"
"github.com/wailsapp/wails/v2/pkg/runtime"
"os"
"path/filepath"
"regexp"
sysRuntime "runtime"
"strconv"
"strings"
"time"
)
@@ -109,6 +107,10 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
`),
}
appOnce.UserDir = filepath.Join(userdir.GetConfigHome(), appOnce.AppName)
err := os.MkdirAll(appOnce.UserDir, 0750)
if err != nil {
fmt.Println("Mkdir UserDir err: ", err.Error())
}
appOnce.LockFile = filepath.Join(appOnce.UserDir, "install.lock")
initLogger()
initConfig()
@@ -123,22 +125,6 @@ ILKEQKmPPzKs7kp/7Nz+2cT3
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
go httpServerOnce.run()
time.AfterFunc(200*time.Millisecond, func() {
if globalConfig.AutoProxy {
appOnce.OpenSystemProxy()
}
})
go func() {
if a.isInstall() {
return
}
err := os.MkdirAll(a.UserDir, 0750)
if err != nil {
return
}
a.installCert()
}()
}
func (a *App) OnExit() {
@@ -146,24 +132,17 @@ func (a *App) OnExit() {
globalLogger.Close()
}
func (a *App) installCert() {
if res, err := systemOnce.installCert(); err != nil {
if sysRuntime.GOOS == "darwin" {
_ = runtime.ClipboardSetText(appOnce.ctx, `echo "输入本地登录密码" && sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "`+systemOnce.CertFile+`" && touch `+a.LockFile+` && echo "安装完成"`)
DialogErr("证书安装失败,请打开终端执行安装(命令已复制到剪切板),err:" + err.Error() + ", " + res)
} else if sysRuntime.GOOS == "windows" && strings.Contains(err.Error(), "Access is denied.") {
DialogErr("首次启用本软件,请使用鼠标右键选择以管理员身份运行")
} else if sysRuntime.GOOS == "linux" && strings.Contains(err.Error(), "Access is denied.") {
DialogErr("证书路径: " + systemOnce.CertFile + ", 请手动安装,安装完成后请执行: touch" + a.LockFile + " err:" + err.Error() + ", " + res)
} else {
globalLogger.Esg(err, res)
DialogErr("err:" + err.Error() + ", " + res)
}
func (a *App) installCert() (string, error) {
out, err := systemOnce.installCert()
if err != nil {
globalLogger.Esg(err, out)
return out, err
} else {
if err := a.lock(); err != nil {
globalLogger.err(err)
globalLogger.Err(err)
}
}
return out, nil
}
func (a *App) OpenSystemProxy() error {

View File

@@ -37,10 +37,11 @@ func initHttpServer() *HttpServer {
func (h *HttpServer) run() {
listener, err := net.Listen("tcp", globalConfig.Host+":"+globalConfig.Port)
if err != nil {
log.Fatalf("无法启动监听: %v", err)
globalLogger.Err(err)
log.Fatalf("Service cannot start: %v", err)
}
fmt.Println("服务已启动,监听 http://" + globalConfig.Host + ":" + globalConfig.Port)
if err := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Service started, listening http://" + globalConfig.Host + ":" + globalConfig.Port)
if err1 := http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Host == "127.0.0.1:"+globalConfig.Port && strings.Contains(r.URL.Path, "/cert") {
w.Header().Set("Content-Type", "application/x-x509-ca-data")
w.Header().Set("Content-Disposition", "attachment;filename=res-downloader-public.crt")
@@ -51,8 +52,9 @@ func (h *HttpServer) run() {
} else {
proxyOnce.Proxy.ServeHTTP(w, r) // 代理
}
})); err != nil {
fmt.Printf("服务器异常: %v", err)
})); err1 != nil {
globalLogger.Err(err1)
fmt.Printf("Service startup exception: %v", err1)
}
}
@@ -118,7 +120,7 @@ func (h *HttpServer) writeJson(w http.ResponseWriter, data ResponseData) {
w.WriteHeader(200)
err := json.NewEncoder(w).Encode(data)
if err != nil {
globalLogger.err(err)
globalLogger.Err(err)
}
}
@@ -206,14 +208,10 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
switch sysRuntime.GOOS {
case "darwin":
// macOS
cmd = exec.Command("open", "-R", filePath)
case "windows":
// Windows
cmd = exec.Command("explorer", "/select,", filePath)
case "linux":
// linux
// 尝试使用不同的文件管理器
cmd = exec.Command("nautilus", filePath)
if err := cmd.Start(); err != nil {
cmd = exec.Command("thunar", filePath)
@@ -222,7 +220,7 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
if err := cmd.Start(); err != nil {
cmd = exec.Command("pcmanfm", filePath)
if err := cmd.Start(); err != nil {
globalLogger.err(err)
globalLogger.Err(err)
h.error(w, err.Error())
return
}
@@ -236,23 +234,45 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
err = cmd.Start()
if err != nil {
globalLogger.err(err)
globalLogger.Err(err)
h.error(w, err.Error())
return
}
h.success(w)
}
func (h *HttpServer) install(w http.ResponseWriter, r *http.Request) {
if appOnce.isInstall() {
h.success(w, respData{
"isPass": systemOnce.Password == "",
})
return
}
out, err := appOnce.installCert()
if err != nil {
h.error(w, err.Error()+"\n"+out, respData{
"isPass": systemOnce.Password == "",
})
return
}
h.success(w, respData{
"isPass": systemOnce.Password == "",
})
}
func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
var data struct {
Password string `json:"password"`
IsCache bool `json:"isCache"`
}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
h.error(w, err.Error())
return
}
systemOnce.SetPassword(data.Password)
systemOnce.SetPassword(data.Password, data.IsCache)
h.success(w)
}
@@ -260,12 +280,12 @@ func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) {
err := appOnce.OpenSystemProxy()
if err != nil {
h.error(w, err.Error(), respData{
"isProxy": appOnce.IsProxy,
"value": appOnce.IsProxy,
})
return
}
h.success(w, respData{
"isProxy": appOnce.IsProxy,
"value": appOnce.IsProxy,
})
}
@@ -273,18 +293,18 @@ func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
err := appOnce.UnsetSystemProxy()
if err != nil {
h.error(w, err.Error(), respData{
"isProxy": appOnce.IsProxy,
"value": appOnce.IsProxy,
})
return
}
h.success(w, respData{
"isProxy": appOnce.IsProxy,
"value": appOnce.IsProxy,
})
}
func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
h.success(w, respData{
"isProxy": appOnce.IsProxy,
"value": appOnce.IsProxy,
})
}

View File

@@ -24,7 +24,7 @@ func (l *Logger) Close() {
_ = l.logFile.Close()
}
func (l *Logger) err(err error) {
func (l *Logger) Err(err error) {
l.Error().Stack().Err(err)
}

View File

@@ -24,10 +24,12 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
return true
}
switch r.URL.Path {
case "/api/preview":
httpServerOnce.preview(w, r)
case "/api/install":
httpServerOnce.install(w, r)
case "/api/set-system-password":
httpServerOnce.setSystemPassword(w, r)
case "/api/preview":
httpServerOnce.preview(w, r)
case "/api/proxy-open":
httpServerOnce.openSystemProxy(w, r)
case "/api/proxy-unset":

View File

@@ -7,8 +7,6 @@ import (
"crypto/x509"
"encoding/json"
"fmt"
"github.com/elazarl/goproxy"
gonanoid "github.com/matoous/go-nanoid/v2"
"io"
"net"
"net/http"
@@ -17,6 +15,9 @@ import (
"strconv"
"strings"
"time"
"github.com/elazarl/goproxy"
gonanoid "github.com/matoous/go-nanoid/v2"
)
type Proxy struct {
@@ -221,16 +222,16 @@ func (p *Proxy) handleWechatRequest(r *http.Request, ctx *goproxy.ProxyCtx) (*ht
}
func (p *Proxy) buildEmptyResponse(r *http.Request) *http.Response {
body := "内容不存在"
body := "The content does not exist"
resp := &http.Response{
Status: "200 OK",
Status: http.StatusText(http.StatusOK),
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(body)),
ContentLength: int64(len(body)),
Request: r,
}
resp.Header.Set("Content-Type", "text/plain")
resp.Header.Set("Content-Type", "text/plain; charset=utf-8")
return resp
}

View File

@@ -1,20 +1,27 @@
package core
import (
"fmt"
"os"
"path/filepath"
"time"
)
type SystemSetup struct {
CertFile string
Password string
CertFile string
CacheFile string
Password string
aesCipher *AESCipher
}
func initSystem() *SystemSetup {
if systemOnce == nil {
systemOnce = &SystemSetup{
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
aesCipher: NewAESCipher("resd48w2d7er95627d447c490a8f02ff"),
CertFile: filepath.Join(appOnce.UserDir, "cert.crt"),
CacheFile: filepath.Join(appOnce.UserDir, "pass.cache"),
}
systemOnce.checkPasswordFile()
}
return systemOnce
}
@@ -35,6 +42,42 @@ func (s *SystemSetup) initCert() ([]byte, error) {
}
}
func (s *SystemSetup) SetPassword(password string) {
func (s *SystemSetup) SetPassword(password string, isCache bool) {
s.Password = password
if isCache {
encrypted, err := s.aesCipher.Encrypt(password)
if err == nil {
err1 := os.WriteFile(s.CacheFile, []byte(encrypted), 0750)
if err1 != nil {
fmt.Println("Failed to write password: ", err1.Error())
}
} else {
fmt.Println("Failed to Encrypt password: ", err.Error())
}
}
}
func (s *SystemSetup) checkPasswordFile() {
fileInfo, err := os.Stat(s.CacheFile)
if err != nil {
return
}
lastModified := fileInfo.ModTime()
oneMonthAgo := time.Now().AddDate(0, -1, 0)
if lastModified.Before(oneMonthAgo) {
os.Remove(s.CacheFile)
return
}
content, err := os.ReadFile(s.CacheFile)
if err != nil {
return
}
password, err := s.aesCipher.Decrypt(string(content))
if err != nil {
return
}
s.Password = password
}

View File

@@ -73,7 +73,7 @@ func (s *SystemSetup) setProxy() error {
}
for _, args := range cmds {
if output, err := s.runCommand(args); err != nil {
errs = errs + "\n output" + string(output) + "err:" + err.Error()
errs = errs + "output:" + string(output) + " err:" + err.Error() + "\n"
fmt.Println("setProxy:", output, " err:", err.Error())
} else {
is = true
@@ -85,7 +85,7 @@ func (s *SystemSetup) setProxy() error {
return nil
}
return fmt.Errorf("failed to set proxy for any active network service, errs: %s", errs)
return fmt.Errorf("failed to set proxy for any active network service, errs:%s", errs)
}
func (s *SystemSetup) unsetProxy() error {
@@ -103,7 +103,7 @@ func (s *SystemSetup) unsetProxy() error {
}
for _, args := range cmds {
if output, err := s.runCommand(args); err != nil {
errs = errs + "\n output" + string(output) + "err:" + err.Error()
errs = errs + "output:" + string(output) + " err:" + err.Error() + "\n"
fmt.Println("unsetProxy:", output, " err:", err.Error())
} else {
is = true
@@ -115,7 +115,7 @@ func (s *SystemSetup) unsetProxy() error {
return nil
}
return fmt.Errorf("failed to unset proxy for any active network service, errs: %s", errs)
return fmt.Errorf("failed to unset proxy for any active network service, errs:%s", errs)
}
func (s *SystemSetup) installCert() (string, error) {
@@ -123,19 +123,7 @@ func (s *SystemSetup) installCert() (string, error) {
if err != nil {
return "", err
}
getPasswordCmd := exec.Command("osascript", "-e", `tell app "System Events" to display dialog "请输入你的电脑密码,用于安装证书文件:" default answer "" with hidden answer`, "-e", `text returned of result`)
passwordOutput, err := getPasswordCmd.Output()
if err != nil {
return string(passwordOutput), err
}
password := strings.TrimSpace(string(passwordOutput))
s.SetPassword(password)
cmd := exec.Command("sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile)
cmd.Stdin = bytes.NewReader([]byte(password + "\n"))
output, err := cmd.CombinedOutput()
output, err := s.runCommand([]string{"sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile})
if err != nil {
return string(output), err
}

View File

@@ -3,10 +3,28 @@
package core
import (
"bytes"
"fmt"
"os/exec"
)
func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
if len(args) == 0 {
return nil, fmt.Errorf("no command provided")
}
var cmd *exec.Cmd
if s.Password != "" {
cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
} else {
cmd = exec.Command(args[0], args[1:]...)
}
output, err := cmd.CombinedOutput()
return output, err
}
func (s *SystemSetup) setProxy() error {
commands := [][]string{
{"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"},
@@ -16,8 +34,10 @@ func (s *SystemSetup) setProxy() error {
{"gsettings", "set", "org.gnome.system.proxy.https", "port", globalConfig.Port},
}
is := false
errs := ""
for _, cmd := range commands {
if err := exec.Command(cmd[0], cmd[1:]...).Run(); err != nil {
if output, err := s.runCommand(cmd); err != nil {
errs = errs + "output:" + string(output) + " err:" + err.Error() + "\n"
fmt.Println(err)
} else {
is = true
@@ -26,12 +46,14 @@ func (s *SystemSetup) setProxy() error {
if is {
return nil
}
return fmt.Errorf("Failed to activate proxy")
return fmt.Errorf("failed to set proxy for any active network service, errs:%s", errs)
}
func (s *SystemSetup) unsetProxy() error {
cmd := []string{"gsettings", "set", "org.gnome.system.proxy", "mode", "none"}
return exec.Command(cmd[0], cmd[1:]...).Run()
output, err := s.runCommand(cmd)
return fmt.Errorf("failed to unset proxy for any active network service, errs output:" + string(output) + " err:" + err.Error())
}
func (s *SystemSetup) installCert() (string, error) {
@@ -41,34 +63,34 @@ func (s *SystemSetup) installCert() (string, error) {
}
actions := [][]string{
{"/usr/local/share/ca-certificates/", "update-ca-certificates"},
{"/usr/share/ca-certificates/trust-source/anchors/", "update-ca-trust"},
{"/usr/share/ca-certificates/trust-source/anchors/", "trust extract-compat"},
{"/etc/pki/ca-trust/source/anchors/", "update-ca-trust"},
{"/etc/ssl/ca-certificates/", "update-ca-certificates"},
{"cp", "-f", s.CertFile, "/usr/local/share/ca-certificates/" + appOnce.AppName + ".crt"},
{"update-ca-certificates"},
{"cp", "-f", s.CertFile, "/usr/share/ca-certificates/trust-source/anchors/" + appOnce.AppName + ".crt"},
{"update-ca-trust"},
{"trust", "extract-compat"},
{"cp", "-f", s.CertFile, "/etc/pki/ca-trust/source/anchors/" + appOnce.AppName + ".crt"},
{"update-ca-trust"},
{"cp", "-f", s.CertFile, "/etc/ssl/ca-certificates/" + appOnce.AppName + ".crt"},
{"update-ca-certificates"},
}
is := false
outs := ""
errs := ""
for _, action := range actions {
dir := action[0]
if err := exec.Command("sudo", "cp", "-f", s.CertFile, dir+appOnce.AppName+".crt").Run(); err != nil {
fmt.Printf("Failed to copy to %s: %v\n", dir, err)
continue
}
cmd := action[1]
if err := exec.Command("sudo", cmd).Run(); err != nil {
fmt.Printf("Failed to refresh certificates using %s: %v\n", cmd, err)
if output, err1 := s.runCommand(action); err1 != nil {
outs += string(output) + "\n"
errs += err1.Error() + "\n"
fmt.Printf("Failed to execute %v: %v\n", action, err1)
continue
}
is = true
}
if !is {
return "", fmt.Errorf("Certificate installation failed")
if is {
return "", nil
}
return "", nil
return outs, fmt.Errorf("Certificate installation failed, errs:%s", errs)
}

View File

@@ -11,9 +11,6 @@ import (
"time"
)
func Empty(data interface{}) {
}
func DialogErr(message string) {
_, _ = runtime.MessageDialog(appOnce.ctx, runtime.MessageDialogOptions{
Type: runtime.ErrorDialog,
@@ -28,14 +25,11 @@ func IsDevelopment() bool {
}
func FileExist(file string) bool {
_, err := os.Stat(file)
info, err := os.Stat(file)
if err != nil {
return false
}
if os.IsNotExist(err) {
return false
}
return true
return !info.IsDir()
}
func CreateDirIfNotExist(dir string) error {
@@ -55,14 +49,6 @@ func TypeSuffix(mime string) (string, string) {
return "", ""
}
func BuildReferer(rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil {
return ""
}
return u.Scheme + "://" + u.Host + "/"
}
func Md5(data string) string {
hashNew := md5.New()
hashNew.Write([]byte(data))