Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
root authored and root committed Dec 16, 2021
0 parents commit 63a09a1
Show file tree
Hide file tree
Showing 15 changed files with 922 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bin/
/host/etc/
/host/usr/
/target/bin/
/target/ui/
/target/*.sh
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM ubuntu

COPY target /var/packages/pan-xunlei-com/target
COPY xunlei-linux /xunlei-linux
COPY host /var/packages/pan-xunlei-com/host

VOLUME [ "/var/packages/pan-xunlei-com/shares" ]

CMD [ "/xunlei-linux" ]
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 迅雷群晖提取版

## 使用说明
> 从迅雷群晖套件中提取出来用于其他Linux设备的迅雷远程下载服务程序。
## 安装

```sh
# 进入临时目录
cd /tmp/
# 下载并解包
wget -O - https://github.com/shuxs/xunlei-from-symo/releases/download/v2.1.0/xunlei-from-syno.x86_64.tar.gz | tar zx
# 安装
./xunlei-from-syno install && rm -f ./xunlei-from-syno
# 安装完成之后就可以用 http://你设备的IP:2345 来访问了。
```

[也可以下载编译好的程序自行琢磨](https://github.com/shuxs/xunlei-from-symo/releases/download/v2.1.0/xunlei-from-syno.v2.1.0.x86_64.tar.gz)

## 控制

```sh
# 启动
systemctl start xunlei-from-syno
# 停止
systemctl stop xunlei-from-syno
# 状态
systemctl status xunlei-from-syno
```

## 过程
> 尽可能的不污染系统, 安装过程她会做以下处理
1. 创建 `/var/packages/pan-xunlei-com` 目录,在里面释放所需的文件和配置。
2. 创建一个名为 `xunlei-from-syno` 的服务,服务文件路径 `/etc/systemd/system/xunlei-from-syno.service`
3. 启动后会写一些文件。
- `/usr/syno/synoman/webman/modules/authenticate.cgi`
- `/etc/synoinfo.conf`
4. 设置 `xunlei-from-syno` 服务开机自启
5. 启动 `xunlei-from-syno` 服务

> 默认配置,请确保不会冲突
1. 下载目录: `/downloads`, 如需改用其他目录,请自行编辑服务文件
2. 网页端口: `2345`, 如需改用其他目录,请自行编辑服务文件

```ini
# 服务文件
[Unit]
Description=迅雷群晖提取版
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=simple
ExecStart=/var/packages/pan-xunlei-com/xunlei-from-syno run --name U-NAS-迅雷 --port 2345 --download-dir=/downloads

TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
```

## 交流

> QQQ: `238097122`
306 changes: 306 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
package main

import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/http/cgi"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/go-chi/render"
)

const (
SYNOPKG_DSM_VERSION_MAJOR = "7"
SYNOPKG_DSM_VERSION_MINOR = "0"
SYNOPKG_DSM_VERSION_BUILD = "1"
SYNOPKG_PKGNAME = "pan-xunlei-com"
SYNOPKG_PKGBASE = "/var/packages/" + SYNOPKG_PKGNAME
SYNOPKG_PKGDEST = SYNOPKG_PKGBASE + "/target"
LAUNCHER_EXE = SYNOPKG_PKGDEST + "/bin/bin/xunlei-pan-cli-launcher.amd64"
LAUNCHER_SOCK = "unix://" + SYNOPKG_PKGDEST + "/var/" + SYNOPKG_PKGNAME + "-launch.sock"
LAUNCHER_VAR = SYNOPKG_PKGDEST + "/var/"
SOCK_FILE = "unix://" + SYNOPKG_PKGDEST + "/var/" + SYNOPKG_PKGNAME + ".sock"
PID_FILE = LAUNCHER_VAR + SYNOPKG_PKGNAME + ".pid"
ENV_FILE = LAUNCHER_VAR + SYNOPKG_PKGNAME + ".env"
LOG_FILE = LAUNCHER_VAR + SYNOPKG_PKGNAME + ".log"
LAUNCH_PID_FILE = LAUNCHER_VAR + SYNOPKG_PKGNAME + "-launch.pid"
LAUNCH_LOG_FILE = LAUNCHER_VAR + SYNOPKG_PKGNAME + "-launch.log"
INST_LOG = LAUNCHER_VAR + SYNOPKG_PKGNAME + "_install.log"

CONFIG_PATH = SYNOPKG_PKGBASE + "/shares/"
HOME_PATH = CONFIG_PATH + ".drive"
)

const (
synoAuthenticatePath = "/usr/syno/synoman/webman/modules/authenticate.cgi"
synoInfoPath = "/etc/synoinfo.conf"
)

type Options struct {
Name string `flag:"name" short:"n" env:"XUNLEI_DEVICE_NAME" usage:"设备名称"`
Port int `flag:"port" short:"p" env:"XUNLEI_PORT" usage:"监听端口"`
Internal bool `flag:"internal" short:"i" env:"XUNLEI_INTERNAL" usage:"仅本机访问"`
DownloadPATH string `flag:"download-dir" short:"d" env:"XUNLEI_DOWNLOAD_PATH" usage:"下载保存目录"`
}

type App struct {
Options
closers []func(ctx context.Context) error
}

func NewApp(options Options) *App {
return &App{Options: options}
}

func (d *App) preStart() {
files := []string{
LAUNCHER_EXE,
SYNOPKG_PKGDEST + "/bin/bin/xunlei-pan-cli.2.1.0.amd64",
SYNOPKG_PKGDEST + "/bin/bin/version",
SYNOPKG_PKGDEST + "/ui/index.cgi",
}

ok := true
log.Println("[启动自检]")
for _, fn := range files {
if _, err := os.Stat(fn); err != nil {
log.Printf("[启动自检] %s: %v", fn, err)
ok = false
}
}
if !ok {
log.Fatal("[启动自检] 失败,请查看上面错误信息")
}

linkIt := func(p string) error {
if _, err := os.Stat(p); err != nil {
if os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(p), 0755)
return os.Symlink(filepath.Join(SYNOPKG_PKGBASE, "host", p), p)
}
return err
}
return nil
}

if err := linkIt(synoAuthenticatePath); err != nil {
log.Fatalf("[启动自检] %v", err)
}

if err := linkIt(synoInfoPath); err != nil {
log.Fatalf("[启动自检] %v", err)
}
log.Println("[启动自检] 完成")
}

func (d *App) Start() *App {
d.preStart()

go func() {
if err := d.startEngine(); err != nil {
log.Printf("%v", err)
}
}()

go func() {
if err := d.startUI(); err != nil {
log.Printf("%v", err)
}
}()

return d
}

func (d *App) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
for _, closer := range d.closers {
if err := closer(ctx); err != nil {
log.Printf("close: %v", err)
}
}
}

func (d *App) address() string {
if d.Internal {
return fmt.Sprintf("127.0.0.1:%d", d.Port)
}
return fmt.Sprintf(":%d", d.Port)
}

func (d *App) getEnv() (environs []string) {
environs = os.Environ()
environs = append(environs, `DriveListen=`+SOCK_FILE)
environs = append(environs, `PLATFORM="`+d.Name+`"`)
environs = append(environs, fmt.Sprintf(`OS_VERSION="dsm %s.%s-%s"`, SYNOPKG_DSM_VERSION_MAJOR, SYNOPKG_DSM_VERSION_MINOR, SYNOPKG_DSM_VERSION_BUILD))
environs = append(environs, `HOME=`+HOME_PATH)
environs = append(environs, `ConfigPath=`+CONFIG_PATH)
environs = append(environs, `DownloadPATH=`+d.DownloadPATH)
environs = append(environs, "SYNOPKG_DSM_VERSION_MAJOR="+SYNOPKG_DSM_VERSION_MAJOR)
environs = append(environs, "SYNOPKG_DSM_VERSION_MINOR="+SYNOPKG_DSM_VERSION_MINOR)
environs = append(environs, "SYNOPKG_DSM_VERSION_BUILD="+SYNOPKG_DSM_VERSION_BUILD)
environs = append(environs, "SYNOPKG_PKGDEST="+SYNOPKG_PKGDEST)
environs = append(environs, "SYNOPKG_PKGNAME="+SYNOPKG_PKGNAME)
environs = append(environs, "SVC_CWD="+SYNOPKG_PKGDEST)
environs = append(environs, "PID_FILE="+PID_FILE)
environs = append(environs, "ENV_FILE="+ENV_FILE)
environs = append(environs, "LOG_FILE="+LOG_FILE)
environs = append(environs, "LAUNCH_LOG_FILE="+LAUNCH_LOG_FILE)
environs = append(environs, "LAUNCH_PID_FILE="+LAUNCH_PID_FILE)
environs = append(environs, "INST_LOG="+LAUNCH_PID_FILE)
environs = append(environs, "GIN_MODE=release")
return
}

// 启动面板
func (d *App) startUI() error {
mux := chi.NewMux()
mux.Use(middleware.Recoverer)
mux.Use(cors.AllowAll().Handler)
mux.Use(middleware.Logger)

// 跳转
mux.Handle("/",
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
http.Redirect(rw, r, "/webman/3rdparty/"+SYNOPKG_PKGNAME+"/index.cgi/", 307)
}),
)

// 迅雷面板CGI
mux.Handle("/webman/3rdparty/"+SYNOPKG_PKGNAME+"/index.cgi/*",
&cgi.Handler{
Path: filepath.Join(SYNOPKG_PKGDEST, "ui/index.cgi"),
Root: SYNOPKG_PKGDEST,
Dir: SYNOPKG_PKGDEST,
Env: d.getEnv(),
InheritEnv: []string{},
Logger: &log.Logger{},
Args: []string{"--dev"},
Stderr: os.Stderr,
},
)

// 迅雷面板资源
ui := filepath.Join(SYNOPKG_PKGDEST, "ui")
static := http.FileServer(http.Dir(ui))
mux.Handle("/webman/3rdparty/"+SYNOPKG_PKGNAME+"/index.cgi/help/*", static)
mux.Handle("/webman/3rdparty/"+SYNOPKG_PKGNAME+"/index.cgi/images/*", static)
mux.Handle("/webman/3rdparty/"+SYNOPKG_PKGNAME+"/index.cgi/texts/*", static)

// Mock群晖登录
mux.Handle("/webman/login.cgi",
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
render.JSON(rw, r, render.M{"SynoToken": ""}) // 77fa4db9f5dc07f21ff20f5446193dd2
}),
)

listenAddress := d.address()
log.Printf("[控制面板] 启动")
log.Printf("[控制面板] 资源目录: %s", ui)
log.Printf("[控制面板] 监听端口: %s", listenAddress)

server := &http.Server{Addr: listenAddress, Handler: mux, BaseContext: func(l net.Listener) context.Context {
log.Printf("[控制面板] 已启动: %s", l.Addr())
return context.Background()
}}

d.closers = append(d.closers, server.Shutdown)
if err := server.ListenAndServe(); err != nil {
if errors.Is(err, http.ErrServerClosed) {
log.Printf("[控制面板] 已退出")
} else {
log.Printf("[控制面板] 已退出: %v", err)
return err
}
}
return nil
}

// 启动服务
func (d *App) startEngine() error {
os.MkdirAll(LAUNCHER_VAR, 0755)
daemon := exec.Command(LAUNCHER_EXE, "--launcher_listen="+LAUNCHER_SOCK, "--pid="+PID_FILE, "--logfile="+LAUNCH_LOG_FILE)
daemon.Dir = SYNOPKG_PKGDEST
daemon.Env = d.getEnv()
daemon.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
daemon.Stderr = os.Stderr
daemon.Stdout = os.Stdout
daemon.Stdin = os.Stdin

log.Printf("[下载引擎] 启动 ")
log.Printf("[下载引擎] 命令行: %s", daemon.String())
if err := daemon.Start(); err != nil {
return err
}

log.Printf("[下载引擎] PID: %d", daemon.Process.Pid)

d.closers = append(d.closers, func(ctx context.Context) error {
return syscall.Kill(-daemon.Process.Pid, syscall.SIGINT)
})

err := daemon.Wait()
if daemon.ProcessState != nil {
log.Printf("[下载引擎] 已退出: %s", daemon.ProcessState.String())
} else {
log.Printf("[下载引擎] 已退出")
}
return err
}

func delFile(p string) error {
n, err := os.Readlink(p)
if err != nil {
return err
}
if strings.HasPrefix(n, SYNOPKG_PKGDEST) {
if err := os.Remove(p); err != nil {
return err
}
// for {
// p = filepath.Dir(p)
// if yes, err := isEmptyDIR(p); !yes {
// return err
// }
// log.Printf("del dir: %s", p)
// if err := os.Remove(p); err != nil {
// return err
// }
// }
}
return nil
}

func isEmptyDIR(dir string) (bool, error) {
f, err := os.Open(dir)
if err != nil {
return false, err
}
names, err := f.Readdirnames(3)
if err != nil {
return false, err
}
return len(names) == 0, nil
}

type devNull struct{}

func (devNull) Write(p []byte) (n int, err error) {
return
}

func (devNull) Read(p []byte) (n int, err error) {
return
}
Loading

0 comments on commit 63a09a1

Please sign in to comment.