Skip to content

Commit

Permalink
Abstract most logic out of main.go and implement help
Browse files Browse the repository at this point in the history
  • Loading branch information
beefsack committed Aug 27, 2020
1 parent 81a7562 commit b42f92d
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 146 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.2.0] - 2020-08-27
### Added
- Started CHANGELOG.md.
- Proper help with -h.
- Options and arguments can be passed by environment variables.
- Go module files for third party deps.

### Removed
- Docker script to pass environment variable as script, as it is now supported
directly.
12 changes: 5 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ FROM golang:1.14

WORKDIR /go/src/script-httpd

COPY main.go .
RUN go build

COPY docker/run.sh .
COPY . .
RUN go get && go build

# By default, script-httpd listens on all interfaces on port 8080
EXPOSE 80
ENV SCRIPT_HTTPD_ADDR :80
ENV ADDR :80

# By default, script-httpd executes /script
ENV SCRIPT_HTTPD_CMD /script
ENV SCRIPT /script

CMD ["./run.sh"]
CMD ["./script-httpd"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ The official Docker image is [beefsack/script-httpd](https://hub.docker.com/r/be

It can be configured using the following environment variables:

* `SCRIPT_HTTPD_ADDR` - the address to listen on inside the container, defaults to `:80`
* `SCRIPT_HTTPD_CMD` - the command to execute, defaults to `/script`
* `ADDR` - the address to listen on inside the container, defaults to `:80`
* `SCRIPT` - the command to execute, defaults to `/script`

#### Mounting script and running official image

Expand Down
6 changes: 0 additions & 6 deletions docker/run.sh

This file was deleted.

8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/beefsack/script-httpd

go 1.15

require (
github.com/mattn/go-shellwords v1.0.10
github.com/namsral/flag v1.7.4-pre
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
136 changes: 5 additions & 131 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,142 +1,16 @@
package main

import (
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"strings"
"sync"
)

/// Server is a simple proxy server to pipe HTTP requests to a subprocess' stdin
/// and the subprocess' stdout to the HTTP response.
type Server struct {
script []string
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Start subprocess
cmd := exec.Command(s.script[0], s.script[1:]...)

// Get handles to subprocess stdin, stdout and stderr
stdinPipe, err := cmd.StdinPipe()
if err != nil {
log.Printf("error accessing subprocess stdin: %v", err)
respError(w)
return
}
defer stdinPipe.Close()
stderrPipe, err := cmd.StderrPipe()
if err != nil {
log.Printf("error accessing subprocess stderr: %v", err)
respError(w)
return
}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
log.Printf("error accessing subprocess stdout: %v", err)
respError(w)
return
}

// Start the subprocess
err = cmd.Start()
if err != nil {
log.Printf("error starting subprocess: %v", err)
respError(w)
return
}

// We use a WaitGroup to wait for all goroutines to finish
wg := sync.WaitGroup{}

// Write request body to subprocess stdin
wg.Add(1)
go func() {
defer func() {
stdinPipe.Close()
wg.Done()
}()
_, err = io.Copy(stdinPipe, r.Body)
if err != nil {
log.Printf("error writing request body to subprocess stdin: %v", err)
respError(w)
return
}
}()

// Read all stderr and write to parent stderr if not empty
wg.Add(1)
go func() {
defer wg.Done()
stderr, err := ioutil.ReadAll(stderrPipe)
if err != nil {
log.Printf("error reading subprocess stderr: %v", err)
respError(w)
return
}
if len(stderr) > 0 {
log.Print(string(stderr))
}
}()

// Read all stdout, but don't write to the response as we need the exit
// status of the subcommand to know our HTTP response code
wg.Add(1)
var stdout []byte
go func() {
defer wg.Done()
so, err := ioutil.ReadAll(stdoutPipe)
stdout = so
if err != nil {
log.Printf("error reading subprocess stdout: %v", err)
respError(w)
return
}
}()

// We must consume stdout and stderr before `cmd.Wait()` as per
// doc and example at https://golang.org/pkg/os/exec/#Cmd.StdoutPipe
wg.Wait()

// Wait for the subprocess to complete
cmdErr := cmd.Wait()
if cmdErr != nil {
// We don't return here because we also want to try to write stdout if
// there was some output
log.Printf("error running subprocess: %v", err)
respError(w)
}

// Write stdout as the response body
_, err = w.Write(stdout)
if err != nil {
log.Printf("error writing response body: %v", err)
}
}

/// respError sends an error response back to the client. Currently this is just
/// a 500 status code.
func respError(w http.ResponseWriter) {
w.WriteHeader(http.StatusInternalServerError)
}
"github.com/beefsack/script-httpd/scripthttpd"
)

func main() {
if len(os.Args) < 2 {
log.Fatal("script-httpd requires a script to execute")
}
script := os.Args[1:]

addr := os.Getenv("SCRIPT_HTTPD_ADDR")
if addr == "" {
addr = ":8080"
}
opts := scripthttpd.ParseConfig()

log.Printf("listening on %s, proxying to %s", addr, strings.Join(script, " "))
log.Fatal(http.ListenAndServe(addr, &Server{
script: script,
}))
log.Printf("listening on %s, proxying to %s", opts.Addr, strings.Join(opts.Script, " "))
log.Fatal(http.ListenAndServe(opts.Addr, &scripthttpd.Server{Opts: opts}))
}
83 changes: 83 additions & 0 deletions scripthttpd/flag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package scripthttpd

import (
"fmt"
"log"
"os"
"strings"

"github.com/mattn/go-shellwords"
"github.com/namsral/flag"
)

const EnvScript = "SCRIPT"

const helpHeader = `
script-httpd is a simple helper to turn a command line script into an HTTP
service.
Homepage: http://github.com/beefsack/script-httpd
script-httpd functions by starting the script for each request and piping the
HTTP request body into stdin of the subprocess. stdout is captured and returned
as the body of the HTTP response.
stderr is not sent to the client, but is logged to the script-httpd process
stderr. stderr can be sent to the client using redirection if required.
The exit status of the script determines the HTTP status code for the response:
200 when the exit status is 0, otherwise 500. Because of this, the response
isn't sent until the script completes.
Example server that responds with the number of lines in the request body:
script-httpd wc -l
Piping and redirection are supported by calling a shell directly:
script-httpd bash -c 'date && wc -l'
All options are also exposed as environment variables, entirely in uppercase.
Eg. -addr can also be specified using the environment variable ADDR. The script
arguments can be passed in the SCRIPT environment variable instead.
Available options:
`

type Opts struct {
Script []string
Addr string
}

func init() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, strings.TrimLeft(helpHeader, "\n"))
flag.PrintDefaults()
}
}

func ParseConfig() Opts {
opts := Opts{}

flag.StringVar(&opts.Addr, "addr", ":8080", "the TCP network address to listen on, eg. ':80'")

flag.Parse()

opts.Script = flag.Args()
if len(opts.Script) == 0 {
envScript := os.Getenv(EnvScript)
if envScript != "" {
args, err := shellwords.Parse(envScript)
if err != nil {
log.Fatalf("error parsing SCRIPT environment variable: %v", err)
}
opts.Script = args
} else {
// No script was passed via args or env, print usage and exit.
flag.Usage()
os.Exit(2)
}
}

return opts
}
Loading

0 comments on commit b42f92d

Please sign in to comment.