GO 实现 cgi 标准

仓库地址 https://gitee.com/xqhero/go.git cgi目录

1. 设计结构体

type Handler struct {
    Path     string         // 程序的路径,可以是绝对路径也可以是相对路径
    Dir     string        // getwd 工作目录
    Args     []string     // 参数
    Env     []string    // 环境变量
    Stderr  io.Writer    // 错误日志记录
    Logger  *log.Logger // 日志句柄

}

2. 核心方法

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 1. 初始化需要传递给cgi程序的标准输入、环境变量
    // 1.1 得到程序的实际路径
    var cwd, path string
    if h.Dir != "" {
        path = h.Path
        cwd = h.Dir
    }else {
        cwd, path = filepath.Split(h.Path)
    }
    // 1.2 环境变量设置
    port := "80"
    pattern := regexp.MustCompile(`:(\d+)$`)
    if matches := pattern.FindStringSubmatch(r.Host); len(matches) > 0 {
        port = matches[1];
    }

    execenv := []string{
        "SERVER_PROTOCOL=HTTP/1.1",
        "SERVER_NAME=" + r.Host,
        "SERVER_PORT=" + port,
        "SERVER_SOFTWARE=go",
        "REQUEST_METHOD=" + r.Method,
        "QUERY_STRING=" + r.URL.RawQuery,
        "REQUEST_URI=" + r.URL.RequestURI(),
        "PATH_INFO=" + r.URL.Path,
        "SCRIPT_NAME=" + r.URL.Path,
        "SCRIPT_FILENAME=" + r.URL.Path,
        "GATEWAY_INTERFACE=1.1",
    }

    if remoteIp, remotePort, err := net.SplitHostPort(r.RemoteAddr); err != nil {
        execenv = append(execenv,"REMOTE_ADDR="+remoteIp,"REMOTE_HOST="+remoteIp, "REMOTE_PORT="+remotePort)
    } else {
        execenv = append(execenv,"REMOTE_ADDR="+r.RemoteAddr,"REMOTE_HOST="+r.RemoteAddr)
    }

    if r.TLS != nil {
        execenv = append(execenv, "HTTPS=on")
    }

    // 1.3 把header中的信息添加到环境变量中
    for k,v := range r.Header {
        k := strings.Map(upperCaseAndUnderscore, k)
        joinStr := ", "
        if k == "COOKIE" {
            joinStr = "; "
        }
        execenv = append(execenv, "HTTP_"+k+"="+strings.Join(v,joinStr))
    }

    if r.ContentLength > 0 {
        execenv = append(execenv, fmt.Sprintf("CONTENT_LENGTH=%d", r.ContentLength))
    }

    if ctype := r.Header.Get("Content-Type"); ctype != "" {
        execenv = append(execenv, "CONTENT_TYPE=" + ctype)
    }

    envPath := os.Getenv("PATH");
    execenv = append(execenv,"PATH="+envPath)
    if h.Env!=nil && len(h.Env) > 0 {
        execenv = append(execenv, h.Env...)
    }

    // 定义一个函数类型的变量
    innerError := func(e error){
        w.WriteHeader(http.StatusInternalServerError)
        h.printf("CGI error: %v", e)
    }
    // 2. 创建子进程,执行子程序
    cmd := &exec.Cmd{
        Path: path,
        Args: append([]string{h.Path},h.Args...),
        Dir:  cwd,
        Env:  execenv,
        Stderr: h.stderr(),  // 设置错误输出句柄
    }
    stdOut, err:= cmd.StdoutPipe()
    if err != nil {
        innerError(err)
        return
    }

    cmd.Stdin = r.Body
    if e := cmd.Start(); e != nil {
        innerError(e)
        return
    }
    defer cmd.Wait()
    defer stdOut.Close()
    // 3. 对返回进行解析
    reader := bufio.NewReader(stdOut)
    headers := make(http.Header)
    statusCode := 0
    headerLines := 0
    sawBlankLine := false

    for {
        // 循环读取数据
        line, isPrefix, err := reader.ReadLine()
        if isPrefix {
            innerError(errors.New("cgi: long header line from subprocess."))
            return
        }
        if err == io.EOF {
            break;
        }
        if err != nil {
            innerError(err)
            return
        }
        // 如果遇到空行,则表示header结束
        if len(line) == 0 {
            sawBlankLine = true
            break
        }
        // header行数递增
        headerLines++
        // 将header行进行拆分
        parts := strings.SplitN(string(line), ":", 2)
        if len(parts) < 2 {
            // 忽略不正确的header
            h.printf("cgi: bad header line: %s", string(line))
            continue
        }

        header, value := parts[0], parts[1]
        header = strings.TrimSpace(header)
        value = strings.TrimSpace(value)
        switch {
        case header == "Status":
            if len(value) < 3 {
                innerError(errors.New("cgi: bad status code"))
                return
            }
            code, err := strconv.Atoi(value[0:3])
            if err != nil {
                innerError(errors.New("cgi: code convert error"))
                return
            }
            statusCode = code
        default:
            headers.Add(header, value)
        }
    }
    if headerLines == 0 || !sawBlankLine {
        innerError(errors.New("cgi: no headers"))
        return
    }

    // 判断是否为location
    if location := headers.Get("Location"); location != "" {
        statusCode = http.StatusFound
    }

    // 设置默认的content-type
    if headers.Get("Content-Type") == "" {
        headers.Set("Content-Type", "text/html; charset=utf8")
    }

    if statusCode == 0 {
        statusCode = http.StatusOK
    }

    for k, vv := range headers {
        for _, v := range vv {
            w.Header().Add(k,v)
        }
    }

    w.WriteHeader(statusCode)

    _, err = io.Copy(w, reader)
    if err != nil {
        h.printf("cgi: copy error: %v", err)
        cmd.Process.Kill()
    }
}

3. 辅助方法

func (h *Handler) printf(format string, v ...interface{}) {
    if h.Logger != nil {
        h.Logger.Printf(format, v...)
    } else {
        log.Printf(format, v...)
    }
}

func upperCaseAndUnderscore(r rune) rune {
    switch {
    case r > 'a' && r < 'z' :
        return r - ('a' - 'A')
    case r == '-':
        return '_'
    case r == '=':
        return '_'
    }
    return r
}

4. 使用示例

type Mymux struct {}

func (m *Mymux) ServeHTTP(w http.ResponseWriter, r *http.Request){
    dir := "/Users/xqhero/goproject/goexample/cgi"
    handler := &cgi.Handler{
        Path: fmt.Sprintf(".%s",r.URL.Path),
        Env:  []string{"A=10"},
        Dir:  dir,
    }
    handler.ServeHTTP(w,r)
}

func main()  {
    mux := &Mymux{}
    e := http.ListenAndServe("127.0.0.1:9800", mux)
    if e != nil {
        fmt.Println("server start error", e)
    }
}
文档更新时间: 2021-01-27 02:15   作者:周国强