This commit is contained in:
程广 2025-09-17 08:46:39 +08:00
commit 64e48a4094
16 changed files with 952 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 kingecg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

185
README.md Normal file
View File

@ -0,0 +1,185 @@
# Gosh
Gosh 是一个轻量级的 HTTP 服务器类库,允许用户通过注册 [Anko](https://github.com/mattn/anko) 脚本和 Go 函数来快速构建 RESTful API。
## 特性
- 简单易用的 HTTP 路由注册
- 支持 GET、POST、PUT、DELETE、PATCH 等 HTTP 方法
- 通过 Anko 脚本语言实现动态 API 处理
- 内置请求和响应辅助工具
- 支持文件系统、网络、进程和 Shell 命令操作
- 跨平台支持Linux、Windows
## 安装
```bash
go get git.kingecg.top/kingecg/gosh
```
## 快速开始
创建一个简单的 HTTP 服务器:
```go
package main
import (
"fmt"
"net/http"
"git.kingecg.top/kingecg/gosh"
)
func main() {
// 创建服务器实例
serverMux := gosh.NewSHMux()
// 注册 Go 函数,可在脚本中调用
serverMux.RegistFunction("hello", func(name string) string {
return fmt.Sprintf("Hello, %s", name)
})
// 注册 GET 路由,使用 Anko 脚本处理请求
serverMux.GET("/hello", `
name = Req.GetQuery("name")
Res.WriteStr(hello(name))
`)
// 启动服务器
http.ListenAndServe(":8089", serverMux)
}
```
## API 参考
### 服务器创建
```go
// 创建新的服务器实例
serverMux := gosh.NewSHMux()
```
### 路由注册
```go
// 注册 HTTP 方法路由
serverMux.GET(pattern, script)
serverMux.POST(pattern, script)
serverMux.PUT(pattern, script)
serverMux.DELETE(pattern, script)
serverMux.PATCH(pattern, script)
```
### 函数注册
```go
// 注册 Go 函数,可在脚本中调用
serverMux.RegistFunction(name, function)
```
### 请求处理
在 Anko 脚本中,可以使用以下对象和方法:
- `Req` - 请求对象
- `Req.GetQuery(key)` - 获取查询参数
- `Req.GetBodyStr()` - 获取请求体字符串
- `Req.GetHeader(key)` - 获取请求头
- `Req.GetBody()` - 获取解析后的请求体JSON 或表单)
- `Res` - 响应对象
- `Res.SetHeader(key, value)` - 设置响应头
- `Res.WriteStr(str)` - 写入字符串响应
- `Res.WriteJSON(value)` - 写入 JSON 响应
- `Res.Status(code)` - 设置状态码
- `Res.StatusOk()` - 设置 200 状态码
- `Res.StatusBadRequest()` - 设置 400 状态码
- `Res.StatusNotFound()` - 设置 404 状态码
- `Res.StatusInternalServerError()` - 设置 500 状态码
- `Res.StatusForbidden()` - 设置 403 状态码
### 内置模块
在 Anko 脚本中,可以使用 `Require` 函数导入以下模块:
- `fs` - 文件系统模块
- 提供文件读写操作
- `net` - 网络模块
- 提供网络相关操作,如端口检测
- `process` - 进程模块
- 提供进程信息和环境变量访问
- 获取和设置环境变量
- 获取进程 ID 和主机名等信息
- `shell` - Shell 命令模块
- 提供执行 Shell 命令的功能
- 支持 Linux bash、Windows cmd 和 PowerShell
- 命令输出解析功能
## 示例
### 处理 JSON 请求
```go
serverMux.POST("/api/user", `
body = Req.GetBody()
name = body["name"]
age = body["age"]
response = {
"message": "User created",
"user": {
"name": name,
"age": age
}
}
Res.WriteJSON(response)
`)
```
### 使用文件系统模块
```go
serverMux.GET("/api/file", `
fs = Require("fs")
content = fs.ReadFile("/path/to/file.txt", "utf8")
Res.WriteStr(content)
`)
```
### 使用进程模块
```go
serverMux.GET("/api/env", `
process = Require("process")
env = process.GetAllEnv()
Res.WriteJSON(env)
`)
```
### 使用 Shell 模块
```go
serverMux.GET("/api/ls", `
shell = Require("shell")
result, err = shell.Exec("ls -la")
if err == nil {
Res.WriteStr(result.Stdout)
} else {
Res.StatusInternalServerError()
Res.WriteStr("Error executing command")
}
`)
```
## 贡献
欢迎提交问题和拉取请求!
## 许可证
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。

21
example/main.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"fmt"
"net/http"
"git.kingecg.top/kingecg/gosh"
)
func main() {
serverMux := gosh.NewSHMux()
serverMux.RegistFunction("hello", func(name string) string {
return fmt.Sprintf("Hello, %s", name)
})
serverMux.GET("/hello", `
name = Req.GetQuery("name")
Res.WriteStr(hello(name))
`)
http.ListenAndServe(":8089", serverMux)
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.kingecg.top/kingecg/gosh
go 1.23.1
require github.com/mattn/anko v0.1.10 // indirect

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/mattn/anko v0.1.10 h1:3QcIxCLirIxOZhIVtvo9eWz8tym/iZ9Nb29VCnzaMvc=
github.com/mattn/anko v0.1.10/go.mod h1:gjrudvzf1t7FWTZo1Nbywnr75g3uDnGjXdp2nkguBjQ=

123
gosh.go Normal file
View File

@ -0,0 +1,123 @@
package gosh
import (
"net/http"
"github.com/mattn/anko/env"
"github.com/mattn/anko/vm"
)
const (
GET = "GET"
POST = "POST"
PUT = "PUT"
DEL = "DELETE"
PATCH = "PATCH"
)
type SHMux struct {
*http.ServeMux
ankoEnv *env.Env
getScripts map[string]string
postScripts map[string]string
putScripts map[string]string
delScripts map[string]string
patchScripts map[string]string
}
func NewSHMux() *SHMux {
return &SHMux{
ServeMux: http.NewServeMux(),
ankoEnv: env.NewEnv(),
}
}
func (s *SHMux) RegistFunction(name string, function interface{}) {
if s.ankoEnv != nil {
s.ankoEnv.Define(name, function)
}
}
func (s *SHMux) register(method, pattern, script string) {
switch method {
case GET:
if s.getScripts == nil {
s.getScripts = make(map[string]string)
}
s.getScripts[pattern] = script
case POST:
if s.postScripts == nil {
s.postScripts = make(map[string]string)
}
s.postScripts[pattern] = script
case PUT:
if s.putScripts == nil {
s.putScripts = make(map[string]string)
}
s.putScripts[pattern] = script
case DEL:
if s.delScripts == nil {
s.delScripts = make(map[string]string)
}
s.delScripts[pattern] = script
case PATCH:
if s.patchScripts == nil {
s.patchScripts = make(map[string]string)
}
s.patchScripts[pattern] = script
}
}
func (s *SHMux) GET(pattern, script string) {
s.register(GET, pattern, script)
}
func (s *SHMux) POST(pattern, script string) {
s.register(POST, pattern, script)
}
func (s *SHMux) PUT(pattern, script string) {
s.register(PUT, pattern, script)
}
func (s *SHMux) DELETE(pattern, script string) {
s.register(DEL, pattern, script)
}
func (s *SHMux) PATCH(pattern, script string) {
s.register(PATCH, pattern, script)
}
func (s *SHMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var script string
switch r.Method {
case GET:
script = s.getScripts[r.URL.Path]
case POST:
script = s.postScripts[r.URL.Path]
case PUT:
script = s.putScripts[r.URL.Path]
case DEL:
script = s.delScripts[r.URL.Path]
case PATCH:
script = s.patchScripts[r.URL.Path]
}
s.handle(w, r, script)
}
func (s *SHMux) handle(w http.ResponseWriter, r *http.Request, script string) {
if script == "" {
http.NotFound(w, r)
return
}
reqHelper := &RequestHelper{
r: r,
}
resHelper := &ResponseHelper{
w: w,
}
env := s.ankoEnv.Copy()
env.Define("Req", reqHelper)
env.Define("Res", resHelper)
_, err := vm.Execute(env, nil, script)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// w.Write([]byte(script))
}

121
helper.go Normal file
View File

@ -0,0 +1,121 @@
package gosh
import (
"encoding/json"
"io/ioutil"
"net/http"
)
type RequestHelper struct {
r *http.Request
}
func (r *RequestHelper) GetQuery(key string) string {
return r.r.URL.Query().Get(key)
}
func (r *RequestHelper) GetBodyStr() string {
body, err := ioutil.ReadAll(r.r.Body)
if err != nil {
return ""
}
return string(body)
}
func (r *RequestHelper) GetHeader(key string) string {
return r.r.Header.Get(key)
}
func (r *RequestHelper) getJSONBody() (map[string]interface{}, error) {
body, err := ioutil.ReadAll(r.r.Body)
if err != nil {
return nil, err
}
m := make(map[string]interface{})
err = json.Unmarshal(body, &m)
if err != nil {
return nil, err
}
return m, nil
}
func (r *RequestHelper) getFormBody() (map[string]interface{}, error) {
err := r.r.ParseForm()
if err != nil {
return nil, err
}
m := make(map[string]interface{})
for key, values := range r.r.Form {
if len(values) == 1 {
m[key] = values[0]
} else {
m[key] = values
}
}
return m, nil
}
func (r *RequestHelper) GetBody() map[string]interface{} {
contentType := r.r.Header.Get("Content-Type")
switch contentType {
case "application/json":
m, err := r.getJSONBody()
if err != nil {
return nil
}
return m
case "application/x-www-form-urlencoded":
m, err := r.getFormBody()
if err != nil {
return nil
}
return m
}
return nil
}
type ResponseHelper struct {
w http.ResponseWriter
}
func (r *ResponseHelper) SetHeader(key, value string) {
r.w.Header().Set(key, value)
}
func (r *ResponseHelper) WriteStr(s string) {
r.w.Write([]byte(s))
}
func (r *ResponseHelper) WriteJSON(v interface{}) error {
r.w.Header().Set("Content-Type", "application/json")
data, err := json.Marshal(v)
if err != nil {
return err
}
_, err = r.w.Write(data)
return err
}
func (r *ResponseHelper) Status(code int) {
r.w.WriteHeader(code)
}
func (r *ResponseHelper) StatusOk() {
r.w.WriteHeader(http.StatusOK)
}
func (r *ResponseHelper) StatusBadRequest() {
r.w.WriteHeader(http.StatusBadRequest)
}
func (r *ResponseHelper) StatusNotFound() {
r.w.WriteHeader(http.StatusNotFound)
}
func (r *ResponseHelper) StatusInternalServerError() {
r.w.WriteHeader(http.StatusInternalServerError)
}
func (r *ResponseHelper) StatusForbidden() {
r.w.WriteHeader(http.StatusForbidden)
}

102
interceptor/filesystem.go Normal file
View File

@ -0,0 +1,102 @@
package interceptor
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type FileModule struct{}
func (f *FileModule) ReadFile(path string, encode string) interface{} {
fileContent, err := os.ReadFile(path)
if err != nil {
panic(err)
}
if encode == "binary" {
return fileContent
}
return string(fileContent)
}
func (f *FileModule) WriteFile(path string, content interface{}, encode string) {
fileContent, err := os.ReadFile(path)
if err != nil {
panic(err)
}
if encode == "binary" {
os.WriteFile(path, fileContent, 0644)
}
os.WriteFile(path, []byte(content.(string)), 0644)
}
func (f *FileModule) RemoveFile(path string) {
os.Remove(path)
}
func (f *FileModule) Exists(path string) bool {
_, err := os.Stat(path)
if err != nil {
return os.IsExist(err)
}
return true
}
func (f *FileModule) IsDir(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}
func (f *FileModule) MakeDir(path string, rescursive bool) {
os.MkdirAll(path, 0755)
}
func (f *FileModule) RemoveDir(path string) {
os.RemoveAll(path)
}
func (f *FileModule) ListDir(path string) []string {
files, err := os.ReadDir(path)
if err != nil {
return nil
}
fileNames := make([]string, 0, len(files))
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
return fileNames
}
func (f *FileModule) Copy(src, dst string) {
if !f.IsDir(dst) {
if f.IsDir(filepath.Dir(dst)) {
bcontent := f.ReadFile(src, "binary")
f.WriteFile(dst, bcontent, "binary")
return
} else {
panic(fmt.Errorf("destination directory not exists"))
}
}
if f.IsDir(src) {
if strings.HasSuffix(dst, "/") {
dst = dst + filepath.Base(src)
f.MakeDir(dst, true)
}
os.CopyFS(dst, os.DirFS(src))
}
// 文件拷贝, 需要先读取文件内容, 然后写入到目标文件中
bcontent := f.ReadFile(src, "binary")
f.WriteFile(dst, bcontent, "binary")
// 拷贝文件属性
fileInfo, err := os.Stat(src)
if err != nil {
panic(err)
}
os.Chmod(dst, fileInfo.Mode())
os.Chtimes(dst, fileInfo.ModTime(), fileInfo.ModTime())
}

View File

@ -0,0 +1,66 @@
package interceptor
import (
"fmt"
"os"
"path/filepath"
"github.com/mattn/anko/env"
"github.com/mattn/anko/vm"
)
type Ankointerceptor struct {
importMap map[string]interface{}
ankoEnv *env.Env
}
func NewAnkointerceptor(ankoEnv *env.Env) *Ankointerceptor {
a := &Ankointerceptor{
importMap: make(map[string]interface{}),
ankoEnv: ankoEnv,
}
a.ankoEnv.Define("println", fmt.Println)
a.importMap["fs"] = &FileModule{}
a.importMap["net"] = &NetModule{}
a.importMap["process"] = &ProcessModule{}
a.importMap["shell"] = &ShellModule{}
return a
}
func (a *Ankointerceptor) Exec(script string) (interface{}, error) {
e := a.ankoEnv.Copy()
e.Define("Require", a.genRequireMethod(script))
e.Define("__filename", script)
e.Define("__dirname", filepath.Dir(script))
scriptbytes, frr := os.ReadFile(script)
if frr != nil {
return nil, frr
}
scriptcode := string(scriptbytes)
result, err := vm.Execute(e, nil, scriptcode)
if err != nil {
return nil, err
}
return result, nil
}
func (a *Ankointerceptor) genRequireMethod(basePath string) interface{} {
return func(s string) interface{} {
if r, ok := a.importMap[s]; ok {
return r
}
if !filepath.IsAbs(s) {
s = filepath.Join(filepath.Dir(basePath), s)
}
if _, ok := a.importMap[s]; ok {
return a.importMap[s]
}
result, err := a.Exec(s)
if err != nil {
panic(err)
}
a.importMap[s] = result
return result
}
}

View File

@ -0,0 +1,24 @@
package interceptor
import (
"path/filepath"
"runtime"
"testing"
"github.com/mattn/anko/env"
)
func TestAnkointerceptor_ExecRequire(t *testing.T) {
// 创建测试环境
e := env.NewEnv()
// 创建拦截器实例
interceptor := NewAnkointerceptor(e)
_, file, _, _ := runtime.Caller(0)
scriptPath := filepath.Join(filepath.Dir(file), "testdata/test_caller.ank")
v, _ := interceptor.Exec(scriptPath)
if v != "Hello, tom from test script" {
t.Errorf("Not equal")
}
}

17
interceptor/net.go Normal file
View File

@ -0,0 +1,17 @@
package interceptor
import (
"fmt"
"net"
)
type NetModule struct{}
func (n *NetModule) IsPortOpen(host string, port int) bool {
conn, err := net.Dial("tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
if err != nil {
return false
}
conn.Close()
return true
}

77
interceptor/process.go Normal file
View File

@ -0,0 +1,77 @@
package interceptor
import (
"os"
"runtime"
"strings"
)
// ProcessModule 提供对进程信息和环境变量的访问
type ProcessModule struct{}
// GetEnv 获取环境变量
func (p *ProcessModule) GetEnv(key string) string {
return os.Getenv(key)
}
// SetEnv 设置环境变量
func (p *ProcessModule) SetEnv(key, value string) error {
return os.Setenv(key, value)
}
// GetAllEnv 获取所有环境变量
func (p *ProcessModule) GetAllEnv() map[string]string {
envMap := make(map[string]string)
for _, env := range os.Environ() {
pair := strings.SplitN(env, "=", 2)
if len(pair) == 2 {
envMap[pair[0]] = pair[1]
}
}
return envMap
}
// GetPid 获取当前进程ID
func (p *ProcessModule) GetPid() int {
return os.Getpid()
}
// GetPpid 获取父进程ID
func (p *ProcessModule) GetPpid() int {
return os.Getppid()
}
// GetHostname 获取主机名
func (p *ProcessModule) GetHostname() (string, error) {
return os.Hostname()
}
// GetOS 获取操作系统类型
func (p *ProcessModule) GetOS() string {
return runtime.GOOS
}
// GetArch 获取系统架构
func (p *ProcessModule) GetArch() string {
return runtime.GOARCH
}
// GetCwd 获取当前工作目录
func (p *ProcessModule) GetCwd() (string, error) {
return os.Getwd()
}
// Chdir 改变当前工作目录
func (p *ProcessModule) Chdir(dir string) error {
return os.Chdir(dir)
}
// GetArgs 获取命令行参数
func (p *ProcessModule) GetArgs() []string {
return os.Args
}
// Exit 退出程序
func (p *ProcessModule) Exit(code int) {
os.Exit(code)
}

171
interceptor/shell.go Normal file
View File

@ -0,0 +1,171 @@
package interceptor
import (
"bytes"
"errors"
"os/exec"
"runtime"
"strings"
)
// ShellModule 提供执行shell命令的功能
type ShellModule struct{}
// CommandResult 表示命令执行结果
type CommandResult struct {
Stdout string
Stderr string
ExitCode int
}
// Exec 执行shell命令并返回结果
func (s *ShellModule) Exec(command string) (*CommandResult, error) {
var cmd *exec.Cmd
var shell, flag string
switch runtime.GOOS {
case "windows":
shell = "cmd"
flag = "/c"
default: // linux, darwin, etc.
shell = "bash"
flag = "-c"
}
cmd = exec.Command(shell, flag, command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
result := &CommandResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: 0,
}
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitError.ExitCode()
} else {
return result, err
}
}
return result, nil
}
// ExecPowerShell 在Windows上执行PowerShell命令
func (s *ShellModule) ExecPowerShell(command string) (*CommandResult, error) {
if runtime.GOOS != "windows" {
return nil, errors.New("PowerShell is only available on Windows")
}
cmd := exec.Command("powershell", "-Command", command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
result := &CommandResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: 0,
}
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitError.ExitCode()
} else {
return result, err
}
}
return result, nil
}
// ExecBash 专门执行Bash命令Linux/macOS
func (s *ShellModule) ExecBash(command string) (*CommandResult, error) {
if runtime.GOOS == "windows" {
return nil, errors.New("Bash is not available on Windows by default")
}
cmd := exec.Command("bash", "-c", command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
result := &CommandResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: 0,
}
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitError.ExitCode()
} else {
return result, err
}
}
return result, nil
}
// ExecCmd 专门执行Windows CMD命令
func (s *ShellModule) ExecCmd(command string) (*CommandResult, error) {
if runtime.GOOS != "windows" {
return nil, errors.New("CMD is only available on Windows")
}
cmd := exec.Command("cmd", "/c", command)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
result := &CommandResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
ExitCode: 0,
}
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
result.ExitCode = exitError.ExitCode()
} else {
return result, err
}
}
return result, nil
}
// ParseOutput 解析命令输出为字符串数组(按行分割)
func (s *ShellModule) ParseOutput(output string) []string {
return strings.Split(strings.TrimSpace(output), "\n")
}
// ParseKeyValue 解析键值对格式的输出(如 KEY=VALUE
func (s *ShellModule) ParseKeyValue(output string) map[string]string {
result := make(map[string]string)
lines := s.ParseOutput(output)
for _, line := range lines {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
return result
}

7
interceptor/testdata/test_caller.ank vendored Normal file
View File

@ -0,0 +1,7 @@
sp=Require("./test_script.ank")
fs=Require("fs")
if fs.Exists(__filename) {
println("fs module is ok")
}
ret=sp("tom")
ret

7
interceptor/testdata/test_script.ank vendored Normal file
View File

@ -0,0 +1,7 @@
// 这是一个测试脚本文件,用于测试 Require 功能
message = "Hello from test script"
c = func(name) {
return "Hello, " + name + " from test script"
}
println(__filename)
c

3
require.md Normal file
View File

@ -0,0 +1,3 @@
# gosh
创建一个http服务器类库。让用户可以通过注册anko脚本和函数来快速构建RESTful API。