From d1fc0036cb4c6b49fc5e68f33d9c3f2d4941be09 Mon Sep 17 00:00:00 2001 From: lejianwen <84855512@qq.com> Date: Tue, 31 Dec 2024 23:16:15 +0800 Subject: [PATCH] feat(server): Add Rustdesk Command And add build full s6 image for rustdesk command --- .github/workflows/build.yml | 52 ++++++++++++ Dockerfile | 3 +- Dockerfile_full_s6 | 38 +++++++++ cmd/apimain.go | 3 +- http/controller/admin/rustdesk.go | 133 +++++++++++++++++++++++------- http/router/admin.go | 11 +++ model/serverCmd.go | 24 ++++++ service/serverCmd.go | 92 +++++++++++++++++++++ service/service.go | 1 + 9 files changed, 322 insertions(+), 35 deletions(-) create mode 100644 Dockerfile_full_s6 create mode 100644 model/serverCmd.go create mode 100644 service/serverCmd.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5494024..08155e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -292,6 +292,21 @@ jobs: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }} labels: ${{ steps.meta.outputs.labels }} + - name: Build and push Docker Full S6 image to Docker Hub ${{ matrix.job.platform }} + if: ${{ env.SKIP_DOCKER_HUB == 'false' }} # Only run this step if SKIP_DOCKER_HUB is false + uses: docker/build-push-action@v5 + with: + context: "." + file: ./Dockerfile_full_s6 + platforms: ${{ matrix.job.docker_platform }} + push: true + provenance: false + build-args: | + BUILDARCH=${{ matrix.job.platform }} + tags: | + ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-${{ matrix.job.platform }} + labels: ${{ steps.meta.outputs.labels }} + - name: Build and push Docker image to GHCR ${{ matrix.job.platform }} if: ${{ env.SKIP_GHCR == 'false' }} # Only run this step if SKIP_GHCR is false uses: docker/build-push-action@v5 @@ -308,6 +323,21 @@ jobs: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:${{ env.TAG }}-${{ matrix.job.platform }} labels: ${{ steps.meta.outputs.labels }} + - name: Build and push Docker Full S6 image to GHCR ${{ matrix.job.platform }} + if: ${{ env.SKIP_GHCR == 'false' }} # Only run this step if SKIP_GHCR is false + uses: docker/build-push-action@v5 + with: + context: "." + file: ./Dockerfile + platforms: ${{ matrix.job.docker_platform }} + push: true + provenance: false + build-args: | + BUILDARCH=${{ matrix.job.platform }} + tags: | + ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-${{ matrix.job.platform }} + labels: ${{ steps.meta.outputs.labels }} + # docker-manifest: name: Push Docker Manifest @@ -378,4 +408,26 @@ jobs: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-armv7l, ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:latest-arm64 push: true + amend: true + + - name: Create and push Full S6 manifest Docker Hub (:version) + if: ${{ env.SKIP_DOCKER_HUB == 'false' }} + uses: Noelware/docker-manifest-action@master + with: + base-image: ${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6 + extra-images: ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64, + ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-armv7l, + ${{ env.DOCKERHUB_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-arm64 + push: true + amend: true + + - name: Create and push Full S6 manifest GHCR (:latest) + if: ${{ env.SKIP_GHCR == 'false' }} + uses: Noelware/docker-manifest-action@master + with: + base-image: ghcr.io/${{ env.BASE_IMAGE_NAMESPACE }}/rustdesk-api:full-s6 + extra-images: ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-amd64, + ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-armv7l, + ghcr.io/${{ env.GHCR_IMAGE_NAMESPACE }}/rustdesk-api:full-s6-arm64 + push: true amend: true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 47cda85..c2482da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,8 @@ FROM alpine ARG BUILDARCH WORKDIR /app -RUN apk add --no-cache tzdata file +RUN apk add --no-cache tzdata COPY ./${BUILDARCH}/release /app/ -RUN file /app/apimain VOLUME /app/data EXPOSE 21114 diff --git a/Dockerfile_full_s6 b/Dockerfile_full_s6 new file mode 100644 index 0000000..3bee5cc --- /dev/null +++ b/Dockerfile_full_s6 @@ -0,0 +1,38 @@ +FROM rustdesk/rustdesk-server-s6:latest as server + +FROM alpine + +ARG BUILDARCH +WORKDIR /app +RUN apk add --no-cache tzdata +COPY ./${BUILDARCH}/release /app/ + +COPY --from=server /init /init +COPY --from=server /etc/s6-overlay /etc/s6-overlay +COPY --from=server /package /package +COPY --from=server /usr/bin/healthcheck.sh /usr/bin/healthcheck.sh +COPY --from=server /usr/bin/hbbr /usr/bin/hbbr +COPY --from=server /usr/bin/hbbs /usr/bin/hbbs +COPY --from=server /usr/bin/rustdesk-utils /usr/bin/rustdesk-utils +COPY --from=server /command /command + +RUN \ + mkdir -p /etc/s6-overlay/s6-rc.d/api && \ + echo -e "key-secret\nhbbs" > /etc/s6-overlay/s6-rc.d/api/dependencies && \ + echo "longrun" > /etc/s6-overlay/s6-rc.d/api/type && \ + echo "#!/command/with-contenv sh" > /etc/s6-overlay/s6-rc.d/api/run && \ + echo "cd /app" >> /etc/s6-overlay/s6-rc.d/api/run && \ + echo "./apimain" >> /etc/s6-overlay/s6-rc.d/api/run && \ + touch /etc/s6-overlay/s6-rc.d/user/contents.d/api && \ + echo "/package/admin/s6/command/s6-svstat /run/s6-rc/servicedirs/api || exit 1" >> /usr/bin/healthcheck.sh && \ + ln -s /run /var/run + +ENV RELAY=relay.example.com +ENV ENCRYPTED_ONLY=0 + +VOLUME /data +VOLUME /app/data + +EXPOSE 21114 21115 21116 21116/udp 21117 21118 21119 + +ENTRYPOINT ["/init"] diff --git a/cmd/apimain.go b/cmd/apimain.go index 0f6f45f..87d7b65 100644 --- a/cmd/apimain.go +++ b/cmd/apimain.go @@ -169,7 +169,7 @@ func InitGlobal() { global.Lock = lock.NewLocal() } func DatabaseAutoUpdate() { - version := 247 + version := 251 db := global.DB @@ -253,6 +253,7 @@ func Migrate(version uint) { &model.AuditFile{}, &model.AddressBookCollection{}, &model.AddressBookCollectionRule{}, + &model.ServerCmd{}, ) if err != nil { fmt.Println("migrate err :=>", err) diff --git a/http/controller/admin/rustdesk.go b/http/controller/admin/rustdesk.go index a21aa1a..9528a17 100644 --- a/http/controller/admin/rustdesk.go +++ b/http/controller/admin/rustdesk.go @@ -2,45 +2,114 @@ package admin import ( "Gwen/global" + "Gwen/http/request/admin" "Gwen/http/response" + "Gwen/model" + "Gwen/service" "github.com/gin-gonic/gin" ) type Rustdesk struct { } -// ServerConfig RUSTDESK服务配置 -// @Tags ADMIN -// @Summary RUSTDESK服务配置 -// @Description 服务配置,给webclient提供api-server -// @Accept json -// @Produce json -// @Success 200 {object} response.Response -// @Failure 500 {object} response.Response -// @Router /admin/server-config [get] -// @Security token -func (r *Rustdesk) ServerConfig(c *gin.Context) { - cf := &response.ServerConfigResponse{ - IdServer: global.Config.Rustdesk.IdServer, - Key: global.Config.Rustdesk.Key, - RelayServer: global.Config.Rustdesk.RelayServer, - ApiServer: global.Config.Rustdesk.ApiServer, - } - response.Success(c, cf) +type RustdeskCmd struct { + Cmd string `json:"cmd"` + Option string `json:"option"` } -// AppConfig APP服务配置 -// @Tags ADMIN -// @Summary APP服务配置 -// @Description APP服务配置 -// @Accept json -// @Produce json -// @Success 200 {object} response.Response -// @Failure 500 {object} response.Response -// @Router /admin/app-config [get] -// @Security token -func (r *Rustdesk) AppConfig(c *gin.Context) { - response.Success(c, &gin.H{ - "web_client": global.Config.App.WebClient, - }) +func (r *Rustdesk) CmdList(c *gin.Context) { + q := &admin.PageQuery{} + if err := c.ShouldBindQuery(q); err != nil { + response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) + return + } + res := service.AllService.ServerCmdService.List(q.Page, 9999) + //在列表前添加系统命令 + list := make([]*model.ServerCmd, 0) + list = append(list, model.SysServerCmds...) + list = append(list, res.ServerCmds...) + res.ServerCmds = list + response.Success(c, res) +} + +func (r *Rustdesk) CmdDelete(c *gin.Context) { + f := &model.ServerCmd{} + if err := c.ShouldBindJSON(f); err != nil { + response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) + return + } + if f.Id == 0 { + response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")) + return + } + + ex := service.AllService.ServerCmdService.Info(f.Id) + if ex.Id == 0 { + response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound")) + return + } + + err := service.AllService.ServerCmdService.Delete(ex) + if err != nil { + response.Fail(c, 101, err.Error()) + return + } + response.Success(c, nil) +} +func (r *Rustdesk) CmdCreate(c *gin.Context) { + f := &model.ServerCmd{} + if err := c.ShouldBindJSON(f); err != nil { + response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) + return + } + errList := global.Validator.ValidStruct(c, f) + if len(errList) > 0 { + response.Fail(c, 101, errList[0]) + return + } + err := service.AllService.ServerCmdService.Create(f) + if err != nil { + response.Fail(c, 101, err.Error()) + return + } + response.Success(c, nil) +} + +func (r *Rustdesk) CmdUpdate(c *gin.Context) { + f := &model.ServerCmd{} + if err := c.ShouldBindJSON(f); err != nil { + response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error()) + return + } + errList := global.Validator.ValidStruct(c, f) + if len(errList) > 0 { + response.Fail(c, 101, errList[0]) + return + } + ex := service.AllService.ServerCmdService.Info(f.Id) + if ex.Id == 0 { + response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound")) + return + } + err := service.AllService.ServerCmdService.Update(f) + if err != nil { + response.Fail(c, 101, err.Error()) + return + } + response.Success(c, nil) +} + +func (r *Rustdesk) SendCmd(c *gin.Context) { + rc := &RustdeskCmd{} + c.ShouldBindJSON(rc) + if rc.Cmd == "" { + response.Fail(c, 101, "cmd is required") + return + } + res, err := service.AllService.ServerCmdService.SendCmd(rc.Cmd, rc.Option) + if err != nil { + response.Fail(c, 101, err.Error()) + return + } + response.Success(c, res) } diff --git a/http/router/admin.go b/http/router/admin.go index 36046de..7a49e05 100644 --- a/http/router/admin.go +++ b/http/router/admin.go @@ -46,9 +46,20 @@ func Init(g *gin.Engine) { ShareRecordBind(adg) MyBind(adg) + RustdeskCmdBind(adg) + //访问静态文件 //g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload")) } + +func RustdeskCmdBind(adg *gin.RouterGroup) { + cont := &admin.Rustdesk{} + rg := adg.Group("/rustdesk") + rg.POST("/sendCmd", cont.SendCmd) + rg.GET("/cmdList", cont.CmdList) + rg.POST("/cmdDelete", cont.CmdDelete) + rg.POST("/cmdCreate", cont.CmdCreate) +} func LoginBind(rg *gin.RouterGroup) { cont := &admin.Login{} rg.POST("/login", cont.Login) diff --git a/model/serverCmd.go b/model/serverCmd.go new file mode 100644 index 0000000..5984457 --- /dev/null +++ b/model/serverCmd.go @@ -0,0 +1,24 @@ +package model + +type ServerCmd struct { + IdModel + Cmd string `json:"cmd" gorm:"default:'';not null;"` + Alias string `json:"alias" gorm:"default:'';not null;"` + Option string `json:"option" gorm:"default:'';not null;"` + Explain string `json:"explain" gorm:"default:'';not null;"` + TimeModel +} + +type ServerCmdList struct { + ServerCmds []*ServerCmd `json:"list"` + Pagination +} + +var SysServerCmds = []*ServerCmd{ + {Cmd: "h", Option: "", Explain: "show help"}, + {Cmd: "relay-servers", Alias: "rs", Option: "", Explain: "set or show relay servers"}, + {Cmd: "ip-blocker", Alias: "ib", Option: "[|] [-]", Explain: "block or unblock ip or show blocked ip"}, + {Cmd: "ip-changes", Alias: "ic", Option: "[|] [-]", Explain: "ip-changes(ic) [|] [-]"}, + {Cmd: "always-use-relay(aur)", Alias: "aur", Option: "[y|n]", Explain: "always use relay"}, + {Cmd: "test-geo", Alias: "tg", Option: " ", Explain: "test geo"}, +} diff --git a/service/serverCmd.go b/service/serverCmd.go new file mode 100644 index 0000000..621c9b6 --- /dev/null +++ b/service/serverCmd.go @@ -0,0 +1,92 @@ +package service + +import ( + "Gwen/global" + "Gwen/model" + "fmt" + "net" + "time" +) + +type ServerCmdService struct{} + +// List +func (is *ServerCmdService) List(page, pageSize uint) (res *model.ServerCmdList) { + res = &model.ServerCmdList{} + res.Page = int64(page) + res.PageSize = int64(pageSize) + tx := global.DB.Model(&model.ServerCmd{}) + tx.Count(&res.Total) + tx.Scopes(Paginate(page, pageSize)) + tx.Find(&res.ServerCmds) + return +} + +// Info +func (is *ServerCmdService) Info(id uint) *model.ServerCmd { + u := &model.ServerCmd{} + global.DB.Where("id = ?", id).First(u) + return u +} + +// Delete +func (is *ServerCmdService) Delete(u *model.ServerCmd) error { + return global.DB.Delete(u).Error +} + +// Create +func (is *ServerCmdService) Create(u *model.ServerCmd) error { + res := global.DB.Create(u).Error + return res +} + +// SendCmd 发送命令 +func (is *ServerCmdService) SendCmd(cmd string, arg string) (string, error) { + //组装命令 + cmd = cmd + " " + arg + res, err := is.SendSocketCmd("v6", cmd) + if err == nil { + return res, nil + } + //v6连接失败,尝试v4 + res, err = is.SendSocketCmd("v4", cmd) + if err == nil { + return res, nil + } + return "", err +} + +// SendSocketCmd +func (is *ServerCmdService) SendSocketCmd(ty string, cmd string) (string, error) { + addr := "[::1]" + tcp := "tcp6" + if ty == "v4" { + tcp = "tcp" + addr = "127.0.0.1" + } + conn, err := net.Dial(tcp, addr+":21115") + if err != nil { + fmt.Printf("connect to id %s server failed: %v\n", ty, err) + return "", err + } + defer conn.Close() + //发送命令 + _, err = conn.Write([]byte(cmd)) + if err != nil { + fmt.Printf("send cmd failed: %v\n", err) + return "", err + } + time.Sleep(100 * time.Millisecond) + //读取返回 + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil && err.Error() != "EOF" { + fmt.Printf("read response failed: %v\n", err) + return "", err + } + return string(buf[:n]), nil +} + +func (is *ServerCmdService) Update(f *model.ServerCmd) error { + return global.DB.Model(f).Updates(f).Error +} diff --git a/service/service.go b/service/service.go index 4e06280..a5dc5e1 100644 --- a/service/service.go +++ b/service/service.go @@ -17,6 +17,7 @@ type Service struct { *LoginLogService *AuditService *ShareRecordService + *ServerCmdService } func New() *Service {