title: GO语言开发自制小工具框架
date: 2021-08-16 11:10:29
tags: Golang


前言

接上一篇使用fyne解决GUI库中文问题之后,打算落地些小工具来提升实战中的需求。本文以执行本地命令为引子探究fyne库的使用方式。

一:优化后的中文问题

上一篇中最后思考部分中提到的解决方式在本文中得以实践,发现该方法确实要比编译二进制到程序中快捷简便的多。

写完这篇文章的时候突然想到可以根据第一种方法的扩展,那就是先找好win、mac、linux等自己需要跨平台系统上字体文件的具体路径,然后使用runtime.GOOS判断操作系统类型,根据类型调用相应的字体文件来达到不需要将字体文件编译进程序即可正常显示中文字体的目的,并且还减少了因字体文件过大导致程序也相应过大的问题。

完整代码如下:

  1. sysType := runtime.GOOS
  2. switch sysType {
  3. case "darwin":
  4. fmt.Printf("当前操作系统为: %s,自动替换字体文件\n", sysType)
  5. os.Setenv("FYNE_FONT", "/System/Library/Fonts/STHeiti Medium.ttc") //设置mac字体环境
  6. case "windows":
  7. fmt.Printf("当前操作系统为: %s,自动替换字体文件\n", sysType)
  8. os.Setenv("FYNE_FONT", "C:\\Windows\\Fonts\\simhei.ttf") //设置win字体环境
  9. case "linux":
  10. fmt.Printf("当前操作系统为: %s,自动替换字体文件\n", sysType)
  11. //os.Setenv("FYNE_FONT", "linux上字体路径") //设置linux字体环境,暂时没找字体,等以后在找
  12. }
  13. defer os.Unsetenv("FYNE_FONT") //取消环境变量

使用switch语句将runtime.GOOS判断出操作系统的类型进行划分,再将提前找好的对应字体文件设置为环境变量,最后的defer语句作为延迟执行取消环境变量。该处fmt.Printf会在终端中打印出提示信息,在实际使用过程中可注释该句。

二:总框架设计

fyne的使用需要先创建应用程序对象和窗口对象,如下代码所示前两行。随后调用整体框架的GUI界面,考虑以后可能需要多种窗口界面,这里我放到了另外一个LoadUI函数中。最后3行是对窗体的已经基本设置,可自行更改。
代码如下:

myapp := app.New()                       //应用程序对象
    mywin := myapp.NewWindow("测试版") //创建窗口对象,标题
    window.LoadUI(mywin)
    mywin.Resize(fyne.NewSize(1000, 600)) //设置初始窗体大小
    mywin.CenterOnScreen() //设置中心位置
    mywin.ShowAndRun()                    //运行程序

在LoadUI中,使用了最为方便的AppTabs(选项卡)方法来设计窗体样式。其结构体如下所示

// AppTabs container is used to split your application into various different areas identified by tabs.
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
// Each item is represented by a button at the edge of the container.
//
// Since: 1.4
type AppTabs struct {
    widget.BaseWidget
    Items           []*TabItem
    OnChanged       func(tab *TabItem)
    current         int
    tabLocation     TabLocation
    isTransitioning bool
}

本文中只需要用到Items和OnChanged来标注tab名称和对应事件。其样式如下:
GO语言开发自制小工具框架 - 图1
代码如下:

//总窗体设计,让main调用加载
func LoadUI(mywin fyne.Window) {
    ////页面布局
    content := container.NewAppTabs(container.NewTabItem("命令执行", execUI(mywin)),
        container.NewTabItem("1", loginBtn1),//以下占位,可忽略
        container.NewTabItem("2", loginBtn2),
        container.NewTabItem("3", nameBox),
        container.NewTabItem("4", Display),
    )
    mywin.SetContent(content)
}

在第一个tab中命名为”命令执行”,并且使用execUI函数来载入选项卡内的界面。

三:命令执行模块界面设计

fyne的界面设计就像洋葱一样一层套一层。上面设计完框架整体的UI界面,接下来就需要对每一个选项卡里的界面进行设计。
这里创建了一个单行文本框cmdin := widget.NewEntry()作为命令输入框,并且使用SetPlaceHolder()来设置只读属性的提示信息。
然后创建了一个多行文本框cmdout := widget.NewMultiLineEntry()作为命令输出显示框,并且使用fyne.TextWrapWord属性将超出窗体最大宽度的文本内容可以用垂直滚动条来展示。
该处设计原本是使用label标签来显示的,但是后面发现执行一条ifconfig命令变将整个窗口撑出了屏幕之外,体验十分不好,且影响使用。后经过查找多方资料和看fyne源码才找到了现在的这种方法。
最后创建了一个响应单击事件的执行命令按钮cmdbtn := widget.NewButton()。在按钮中需要传入两个参数,一个是该按钮的显示名称和一个单击事件函数,如下代码所示:
在单击事件的匿名函数中调用了command.ExecForSys(),该函数接收cmdin中的文本作为命令执行并将结果输出(后面会介绍该函数)。随后cmdout.SetText(output)将输出的结果显示到cmdout中。

func execUI(mywin fyne.Window) fyne.CanvasObject {
    ////命令执行模块
    cmdin := widget.NewEntry()
    cmdin.SetPlaceHolder("commands")

    cmdout := widget.NewMultiLineEntry()
    cmdout.Wrapping = fyne.TextWrapWord

    cmdbtn := widget.NewButton("执行", func() {
        output := command.ExecForSys(cmdin.Text)
        cmdout.SetText(output)
    })
    //将按钮和输入框设置在同一行,设置按钮最小大小在右边,输入框自动填充
    oneline := fyne.NewContainerWithLayout(layout.NewBorderLayout(nil, nil, nil, cmdbtn), cmdin, cmdbtn)
    //设置oneline位置最小大小在上,输出框自动填充
    return fyne.NewContainerWithLayout(layout.NewBorderLayout(oneline, nil, nil, nil), oneline, cmdout)
}

oneline这句
layout.NewBorderLayout()函数使得传入的控件放入上、下、左、右位置,并且将其他未传入的控件自动延展满整个布局。使用该函数可以将命令输入的单行文本框cmdin和按钮控件cmdbtn放在同一布局,并且将cmdbtn该布局的最右边,而左侧单行文本框则自动拉伸。
所得到的样式如下:
GO语言开发自制小工具框架 - 图2
如不使用的话,就会每个控件单独一行,如下所示:
GO语言开发自制小工具框架 - 图3
最后返回了一个整体布局,在return中,将单行文本框cmdin和按钮控件cmdbtn作为一个整体,与显示结果的多行文本框cmdout进行上下排列。
整体样式如下:
GO语言开发自制小工具框架 - 图4

四:执行命令功能的实现

上面做好整体的样式布局,剩下的就实现command.ExecForSys()函数的功能即可。
因为不同的操作系统命令有所不同,所以这里还是使用runtime.GOOS来判断系统类型,如下代码所示:
fallthrough用来强制执行后面的case中的代码,即macOS执行与linux同样一套代码。
另外在实验windows的时候,发现如果没有使用chcp 65001编码,那么输出的结果将会导致程序溢出而崩溃。
将传入的命令拼接好在使用exec.Command执行命令再将标准输出return.

func osExec(cmdin string) (output string) {
    var cmds []string
    cmdinPrefix := ""
    switch runtime.GOOS {
    case "darwin":
        fallthrough
    case "linux":
        cmds = append(cmds, "/bin/sh", "-c")
    case "windows":
        cmds = append(cmds, "cmd", "/c")
        cmdinPrefix = "chcp 65001 && "
    }
    cmds = append(cmds, cmdinPrefix+cmdin)
    c := exec.Command(cmds[0], cmds[1:]...)
    // 获取输出对象,可以从该对象中读取输出结果
    stdout, err := c.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    // 运行命令
    if err := c.Start(); err != nil {
        log.Fatal(err)
    }
    outputBytes, _ := ioutil.ReadAll(stdout)
    return string(outputBytes)
}

函数大体结构如此。command包的完整代码如下,即添加了一下置顶的tips。然后合并了tips()和osExec()输出。

package command
import (
    "io/ioutil"
    "log"
    "os/exec"
    "runtime"
    "time"
)
func tips() (tip string) {
    timeUnix := time.Now().Unix() //已知的时间戳
    formatTimeStr := time.Unix(timeUnix, 0).Format("2006-01-02 15:04:05")
    tip = string("操作系统:" + runtime.GOOS + "\n" +
        "系统架构:" + runtime.GOARCH + "\n" +
        "系统时间:" + formatTimeStr + "\n" +
        "\n")
    return tip
}
func ExecForSys(cmdin string) (output string) {
    return tips() + osExec(cmdin)
}
func osExec(cmdin string) (output string) {
    var cmds []string
    cmdinPrefix := ""
    switch runtime.GOOS {
    case "darwin":
        fallthrough
    case "linux":
        cmds = append(cmds, "/bin/sh", "-c")
    case "windows":
        cmds = append(cmds, "cmd", "/c")
        cmdinPrefix = "chcp 65001 && "
    }
    cmds = append(cmds, cmdinPrefix+cmdin)
    c := exec.Command(cmds[0], cmds[1:]...)
    // 获取输出对象,可以从该对象中读取输出结果
    stdout, err := c.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    // 运行命令
    if err := c.Start(); err != nil {
        log.Fatal(err)
    }
    outputBytes, _ := ioutil.ReadAll(stdout)
    return string(outputBytes)
}

五:整体效果

以上整体的框架所展示的效果如下。各位可以根据该篇思路来编写自己的工具形成武器库扩展。
GO语言开发自制小工具框架 - 图5

六:思考

在该篇文章中,主要体现fyne的使用方法和设计思路。关于执行命令的模块编写只是一个引子,该执行本地命令模块个人觉得没啥卵用,各位可以抛砖引玉应用到POC验证、EXP利用等方面会有更好的效果。另外我也在思考如何将执行本地命令模块拆分成含有服务端、用户端、agent端的C2式模块框架,目前还在研究当中…