-
-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Abstract most logic out of main.go and implement help
- Loading branch information
Showing
9 changed files
with
248 additions
and
146 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.