summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile80
-rw-r--r--README.adoc13
-rw-r--r--go.mod8
-rw-r--r--go.sum4
-rw-r--r--src/glaze.go277
-rw-r--r--src/lib.go187
-rw-r--r--src/main.go (renamed from src/cmd/main.go)2
-rwxr-xr-xtests/cli-opts.sh46
-rw-r--r--tests/glaze.go36
-rwxr-xr-xtests/integration.sh73
-rw-r--r--tests/lib.sh119
-rw-r--r--tests/lib_test.go13
-rw-r--r--tests/main.go7
-rw-r--r--tests/resources/index.html4
l---------tests/resources/link.txt1
-rw-r--r--tests/resources/original.txt10
17 files changed, 648 insertions, 235 deletions
diff --git a/.gitignore b/.gitignore
index 85a298a..75d542a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
/*.bin
+/src/*.a
+/src/*.bin
+/tests/*.a
/tests/*.bin
diff --git a/Makefile b/Makefile
index 0893458..59fdddc 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ LANGUAGES = en
PREFIX = /usr
BINDIR = $(PREFIX)/bin
LIBDIR = $(PREFIX)/lib
+GOLIBDIR = $(LIBDIR)/go
INCLUDEDIR = $(PREFIX)/include
SRCDIR = $(PREFIX)/src/$(NAME)
SHAREDIR = $(PREFIX)/share
@@ -21,7 +22,7 @@ LDLIBS =
.SUFFIXES:
-.SUFFIXES: .go .bin
+.SUFFIXES: .go .a .bin .bin-check
@@ -29,17 +30,25 @@ all:
include deps.mk
+objects = \
+ src/$(NAME).a \
+ src/main.a \
+ tests/$(NAME).a \
+ tests/main.a \
+
sources = \
- src/lib.go \
- src/cmd/main.go \
+ src/$(NAME).go \
+ src/main.go \
derived-assets = \
+ $(objects) \
+ src/main.bin \
+ tests/main.bin \
$(NAME).bin \
- tests/lib_test.bin \
side-assets = \
- glaze.socket \
+ $(NAME).socket \
@@ -48,22 +57,46 @@ side-assets = \
all: $(derived-assets)
-$(NAME).bin: src/lib.go src/cmd/main.go Makefile
- go build -o $@ -v src/cmd/main.go
+$(objects): Makefile
+
+src/$(NAME).a: src/$(NAME).go
+ go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go
+
+tests/$(NAME).a: tests/$(NAME).go src/$(NAME).go
+ go tool compile $(GOCFLAGS) -o $@ -p $(*F) $*.go src/$(*F).go
+
+src/main.a: src/main.go src/$(NAME).a
+tests/main.a: tests/main.go tests/$(NAME).a
+src/main.a tests/main.a:
+ go tool compile $(GOCFLAGS) -o $@ -I $(@D) $*.go
-tests/lib_test.bin: src/lib.go tests/lib_test.go Makefile
- go test -c -o $@ -v $*.go
+src/main.bin: src/main.a
+tests/main.bin: tests/main.a
+src/main.bin tests/main.bin:
+ go tool link $(GOLDFLAGS) -o $@ -L $(@D) $*.a
+$(NAME).bin: src/main.bin
+ ln -fs $? $@
-check-unit: tests/lib_test.bin
- ./tests/lib_test.bin
+
+tests.bin-check = \
+ tests/main.bin-check \
+
+tests/main.bin-check: tests/main.bin
+$(tests.bin-check):
+ $(EXEC)$*.bin
+
+check-unit: $(tests.bin-check)
integration-tests = \
tests/cli-opts.sh \
+ tests/integration.sh \
-$(integration-tests): $(NAME).bin ALWAYS
+.PRECIOUS: $(integration-tests)
+$(integration-tests): $(NAME).bin
+$(integration-tests): ALWAYS
sh $@
check-integration: $(integration-tests)
@@ -87,27 +120,32 @@ clean:
install: all
mkdir -p \
'$(DESTDIR)$(BINDIR)' \
+ '$(DESTDIR)$(GOLIBDIR)' \
+ '$(DESTDIR)$(SRCDIR)' \
cp $(NAME).bin '$(DESTDIR)$(BINDIR)'/$(NAME)
- for f in $(sources); do \
- dir='$(DESTDIR)$(SRCDIR)'/"`dirname "$${f#src/}"`"; \
- mkdir -p "$$dir"; \
- cp -P "$$f" "$$dir"; \
- done
+ cp src/$(NAME).a '$(DESTDIR)$(GOLIBDIR)'
+ cp $(sources) '$(DESTDIR)$(SRCDIR)'
## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror
## of the "install" target, and removes *all* that was installed.
## A dedicated test asserts that this is always true.
uninstall:
rm -rf \
- '$(DESTDIR)$(BINDIR)'/$(NAME) \
- '$(DESTDIR)$(SRCDIR)' \
+ '$(DESTDIR)$(BINDIR)'/$(NAME) \
+ '$(DESTDIR)$(GOLIBDIR)'/$(NAME).a \
+ '$(DESTDIR)$(SRCDIR)' \
+
## Run it locally.
run: all
- rm -f glaze.socket
- ./$(NAME).bin -P '/api/socket*:retcp.socket' -P '*:../papo/src/static/' glaze.socket
+ rm -f $(NAME).socket
+ ./$(NAME).bin \
+ -P '/fonte/*:src/' \
+ -P '/pasta/*:tests/' \
+ -P '/api/socket*:retcp.socket' \
+ glaze.socket
ALWAYS:
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..ca9c5ce
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,13 @@
+## Run it locally.
+run: all
+ rm -f glaze.socket
+ ./$(NAME).bin -P '/api/socket*:retcp.socket' -P '*:../papo/src/static/' glaze.socket
+
+glaze -P 'URL:PATH'
+
+
+setfattr -n user.glaze.directory-listing -v true tests
+setfattr -n user.glaze.try-index-html -v true tests
+
+
+add test with directory traversal, and escaping symllinks
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 8787ecf..0000000
--- a/go.mod
+++ /dev/null
@@ -1,8 +0,0 @@
-module euandre.org/glaze
-
-go 1.21.5
-
-require (
- github.com/pkg/xattr v0.4.9 // indirect
- golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index a69db87..0000000
--- a/go.sum
+++ /dev/null
@@ -1,4 +0,0 @@
-github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
-github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
-golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
-golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/src/glaze.go b/src/glaze.go
new file mode 100644
index 0000000..d0bc3e0
--- /dev/null
+++ b/src/glaze.go
@@ -0,0 +1,277 @@
+package glaze
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/fs"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "syscall"
+
+
+ g "gobang"
+)
+
+
+
+type patternPath struct {}
+
+type pathInfo struct {
+ pattern string
+ path string
+ label string
+ fileHandle *os.File
+ fileInfo fs.FileInfo
+}
+
+
+
+const (
+ xattrTryIndexHtml = "user.glaze.try-index-html"
+ xattrDirectoryListing = "user.glaze.directory-listing"
+ xattrExpectedValue = "true"
+)
+
+
+
+func httpError(w http.ResponseWriter, code int, err error) {
+ t := http.StatusText(code)
+ g.Error(
+ err.Error(), "http-server-error",
+ "code", code,
+ "error", err,
+ "status-text", t,
+ )
+ http.Error(w, t, code)
+}
+
+func withTrailingSlash(path string) string {
+ if strings.HasSuffix(path, "/") {
+ return path
+ } else {
+ return path + "/"
+ }
+}
+
+func adjustPattern(pattern string) string {
+ if pattern == "*" {
+ return "/"
+ } else if strings.HasSuffix(pattern, "*") {
+ return pattern[0:len(pattern) - 1]
+ } else {
+ return pattern + "{$}"
+ }
+}
+
+func handleSymlink(info pathInfo, w http.ResponseWriter, r *http.Request) error {
+ linked, err := os.Readlink(info.path)
+ if err != nil {
+ return err
+ }
+
+ // From http.Redirect():
+ //
+ // > If the Content-Type header has not been set,
+ // > Redirect sets it to "text/html; charset=utf-8" and
+ // > writes a small HTML body.
+ //
+ // 🙄
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+
+ http.Redirect(w, r, linked, http.StatusMovedPermanently)
+ return nil
+}
+
+func handleProxy(info pathInfo, w http.ResponseWriter, r *http.Request) error {
+ target, err := url.Parse(info.path)
+ if err != nil {
+ return err
+ }
+
+ target.Scheme = "http"
+ target.Host = "localhost"
+ httpClient := http.Client {
+ Transport: &http.Transport {
+ DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
+ return net.Dial("unix", info.path)
+ },
+ },
+ }
+
+ r.URL.Scheme = target.Scheme
+ r.URL.Host = target.Host
+ r.RequestURI = ""
+ response, err := httpClient.Do(r)
+ if err != nil {
+ return err
+ }
+ for k, vArr := range response.Header {
+ for _, v := range vArr {
+ w.Header().Add(k, v)
+ }
+ }
+ w.WriteHeader(response.StatusCode)
+ io.Copy(w, response.Body)
+ return nil
+}
+
+func handleFile(info pathInfo, w http.ResponseWriter, r *http.Request) error {
+ http.ServeContent(w, r, info.fileInfo.Name(), info.fileInfo.ModTime(), info.fileHandle)
+ return nil
+}
+
+func getXattr(path string, name string) (string, error) {
+ data := make([]byte, 256)
+ i, err := syscall.Getxattr(path, name, data)
+ fmt.Println("i", i)
+ if err != nil {
+ return "", err
+ }
+ return string(data[0:i]), nil
+}
+
+func handleDirectoryTarget(info pathInfo, w http.ResponseWriter, r *http.Request) error {
+ targetPath := withTrailingSlash(info.path) + r.URL.Path
+ newInfo, fn, err := handlerForDynPath(targetPath)
+ if err != nil {
+ return err
+ }
+
+ if (newInfo.label != "directory") {
+ return fn(newInfo, w, r)
+ }
+
+ indexHtmlXattr, err := getXattr(targetPath, xattrTryIndexHtml)
+ if err == nil && string(indexHtmlXattr) == xattrExpectedValue {
+ newPath := withTrailingSlash(targetPath) + "index.html"
+ _, err := os.Open(newPath)
+ if err == nil {
+ return fn(newInfo, w, r)
+ }
+ }
+
+ dirListXattr, err := getXattr(targetPath, xattrDirectoryListing)
+ if err == nil && string(dirListXattr) == xattrExpectedValue {
+ http.FileServer(http.Dir(newInfo.path)).ServeHTTP(w, r)
+ return nil
+ }
+
+ http.Error(w, "Forbidden", http.StatusForbidden)
+ return nil
+}
+
+func handlerFuncFor(mode fs.FileMode) (string, func(pathInfo, http.ResponseWriter, *http.Request) error) {
+ if mode.Type() == fs.ModeSymlink {
+ return "symlink", handleSymlink
+ } else if mode.Type() == fs.ModeSocket {
+ return "socket", handleProxy
+ } else if !mode.IsDir() {
+ return "file", handleFile
+ } else {
+ return "directory", handleDirectoryTarget
+ }
+}
+
+func handlerForDynPath(path string) (pathInfo, func(pathInfo, http.ResponseWriter, *http.Request) error, error) {
+ var info pathInfo
+ fileHandle, err := os.Open(path)
+ if err != nil {
+ return info, nil, err
+ }
+
+ fileInfo, err := os.Lstat(path)
+ if err != nil {
+ return info, nil, err
+ }
+
+ label, fn := handlerFuncFor(fileInfo.Mode())
+ g.Info("Handler picked", "handler-picked", "label", label)
+ info = pathInfo {
+ path: path,
+ label: label,
+ fileHandle: fileHandle,
+ fileInfo: fileInfo,
+ }
+ return info, fn, nil
+}
+
+func handlerFor(path string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ id := g.NewUUID()
+ g.Info("in-request", "in-request", "path", path, "id", id)
+ defer g.Info("in-response", "in-response", "path", path, "id", id)
+
+ info, fn, err := handlerForDynPath(path)
+ if err != nil {
+ httpError(w, http.StatusInternalServerError, err)
+ return
+ }
+
+ err = fn(info, w, r)
+ if err != nil {
+ httpError(w, http.StatusInternalServerError, err)
+ return
+ }
+ })
+}
+
+func (i *patternPath) String() string {
+ return ""
+}
+
+func (_ *patternPath) Set(value string) error {
+ arr := strings.Split(value, ":")
+ if len(arr) != 2 {
+ return errors.New("Bad value for path pattern: " + value)
+ }
+
+ pattern := adjustPattern(arr[0])
+ path := arr[1]
+
+ http.Handle(pattern, http.StripPrefix(pattern, handlerFor(path)))
+ return nil
+}
+
+func parseArgs(args []string) string {
+ var pat patternPath
+ fs := flag.NewFlagSet(args[0], flag.ExitOnError)
+ fs.Var(&pat, "P", "")
+ fs.Parse(args[1:])
+ if fs.NArg() != 1 {
+ fmt.Fprintf(
+ os.Stderr,
+ "Usage: %s [ -P PATTERN:PATH ]... LISTEN.socket\n",
+ args[0],
+ )
+ os.Exit(2)
+ }
+ return fs.Arg(0)
+}
+
+func listen(fromAddr string) net.Listener {
+ listener, err := net.Listen("unix", fromAddr)
+ g.FatalIf(err)
+ g.Info("Started listening", "listen-start", "from-address", fromAddr)
+ return listener
+}
+
+func start(listener net.Listener) {
+ server := http.Server {}
+ err := server.Serve(listener)
+ g.FatalIf(err)
+}
+
+
+
+func Main() {
+ g.Init()
+ addr := parseArgs(os.Args)
+ listener := listen(addr)
+ start(listener)
+}
diff --git a/src/lib.go b/src/lib.go
deleted file mode 100644
index 6b486f5..0000000
--- a/src/lib.go
+++ /dev/null
@@ -1,187 +0,0 @@
-package glaze
-
-import (
- "context"
- "errors"
- "flag"
- "io"
- "io/fs"
- "log"
- "net"
- "net/http"
- "net/url"
- "os"
- "strings"
-
- "github.com/pkg/xattr"
-)
-
-
-
-const TRY_INDEX_HTML_XATTR = "user.glaze.directory-listing"
-
-
-type PatternPath struct {}
-
-func (i *PatternPath) String() string {
- return "FIXME"
-}
-
-func proxyHandler(path string) http.Handler {
- target, err := url.Parse(path)
- if err != nil {
- log.Fatal(err)
- }
-
- target.Scheme = "http"
- target.Host = "localhost"
-
- httpClient := http.Client {
- Transport: &http.Transport {
- DialContext: func(_ context.Context, _ string, _ string) (net.Conn, error) {
- return net.Dial("unix", path)
- },
- },
- }
-
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- r.URL.Scheme = target.Scheme
- r.URL.Host = target.Host
- r.RequestURI = ""
-
- response, err := httpClient.Do(r)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- log.Println(err)
- return
- }
-
- for k, vArr := range response.Header {
- for _, v := range vArr {
- w.Header().Add(k, v)
- }
- }
- w.WriteHeader(response.StatusCode)
- io.Copy(w, response.Body)
- })
-}
-
-func fileHandler(path string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- fileHandle, err := os.Open(path)
- if err != nil {
- http.Error(w, "Not Founde", http.StatusNotFound)
- return
- }
-
- fileInfo, err := fileHandle.Stat()
- if err != nil {
- http.Error(w, "Server Errore", http.StatusInternalServerError)
- return
- }
-
- http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), fileHandle)
- })
-}
-
-func directoryHandler(path string) http.Handler {
- // FIXME: return 301/302 on symlink
- return http.FileServer(http.Dir(path))
-}
-
-func adjustPattern(pattern string) string {
- if pattern == "*" {
- return "/"
- } else if strings.HasSuffix(pattern, "*") {
- return pattern[0:len(pattern) - 1]
- } else {
- return pattern
- // FIXME: when on Go 1.22, use:
- // return pattern + "{$}"
- }
-}
-
-func registerHandler(tag string, pattern string, handler http.Handler) {
- http.Handle(
- pattern,
- logged(
- tag,
- http.StripPrefix(
- pattern,
- handler,
- ),
- ),
- )
-}
-
-func registerPattern(fileInfo fs.FileInfo, pattern string, path string) {
- if fileInfo.Mode().Type() == fs.ModeSocket {
- registerHandler("proxy", pattern, proxyHandler(path))
- } else if !fileInfo.Mode().IsDir() {
- registerHandler("file", pattern, fileHandler(path))
- } else {
- registerHandler("directory", pattern, directoryHandler(path))
-
- data, err := xattr.Get(path, TRY_INDEX_HTML_XATTR)
- if err != nil {
- log.Fatal(err)
- }
- if string(data) == "true" {
- indexPattern := pattern
- if !strings.HasSuffix(indexPattern, "/") {
- indexPattern += "/"
- }
- indexPattern += "index.html"
- registerHandler("index-html-file", indexPattern, fileHandler(path + "/index.html"))
- }
- }
-}
-
-func (_ *PatternPath) Set(value string) error {
- arr := strings.Split(value, ":")
- if len(arr) != 2 {
- return errors.New("Bad value for path pattern: " + value)
- }
-
- pattern := adjustPattern(arr[0])
- path := arr[1]
-
- fileInfo, err := os.Stat(path)
- if err != nil {
- return err
- }
-
- registerPattern(fileInfo, pattern, path)
- return nil
-}
-
-func logged(tag string, next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- log.Printf("%s: %s %s\n", tag, r.Method, r.URL.Path)
- next.ServeHTTP(w, r)
- })
-}
-
-// FIXME: log to json
-func Main() {
- var myFlags PatternPath
- flag.Var(&myFlags, "P", "URL:PATH pattern")
- flag.Parse()
-
- if flag.NArg() != 1 {
- flag.Usage()
- os.Exit(2)
- }
- listenPath := flag.Arg(0)
-
- listener, err := net.Listen("unix", listenPath)
- if err != nil {
- log.Fatal("Failed to net.listen(): " + err.Error())
- }
-
- server := http.Server {}
- err = server.Serve(listener)
- if err != nil {
- log.Fatal(err)
- }
-}
diff --git a/src/cmd/main.go b/src/main.go
index 8c2fbe3..15f5d79 100644
--- a/src/cmd/main.go
+++ b/src/main.go
@@ -1,6 +1,6 @@
package main
-import "euandre.org/glaze/src"
+import "glaze"
func main() {
glaze.Main()
diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh
index fcb62ca..96d4726 100755
--- a/tests/cli-opts.sh
+++ b/tests/cli-opts.sh
@@ -1,4 +1,48 @@
#!/bin/sh
set -eu
-exit
+. tests/lib.sh
+
+
+test_needs_2_arguments() {
+ testing 'needs 2 arguments'
+
+ N="$LINENO"
+ OUT="$(mkstemp)"
+ ERR="$(mkstemp)"
+ trap 'rm -f "$OUT" "$ERR"' EXIT
+ STATUS=0
+ ./binder.bin 1>"$OUT" 2>"$ERR" || STATUS=$?
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
+ rm -f "$OUT" "$ERR"
+
+ N="$LINENO"
+ OUT="$(mkstemp)"
+ ERR="$(mkstemp)"
+ trap 'rm -f "$OUT" "$ERR"' EXIT
+ STATUS=0
+ ./binder.bin FROM-ADDR 1>"$OUT" 2>"$ERR" || STATUS=$?
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
+ rm -f "$OUT" "$ERR"
+
+ N="$LINENO"
+ OUT="$(mkstemp)"
+ ERR="$(mkstemp)"
+ trap 'rm -f "$OUT" "$ERR"' EXIT
+ STATUS=0
+ ./binder.bin TO-ADDR 1>"$OUT" 2>"$ERR" || STATUS=$?
+ assert_status 2
+ assert_empty_stdout
+ assert_usage "$ERR"
+ rm -f "$OUT" "$ERR"
+
+ test_ok
+}
+
+
+exit # FIXME
+test_needs_2_arguments
diff --git a/tests/glaze.go b/tests/glaze.go
new file mode 100644
index 0000000..366efa0
--- /dev/null
+++ b/tests/glaze.go
@@ -0,0 +1,36 @@
+package glaze
+
+import (
+ g "gobang"
+)
+
+
+func test_adjustPattern() {
+ inputs := []string {
+ "",
+ "/",
+ "/*",
+ "/abc*",
+ "/abc/",
+ "/abc/*",
+ "abc/*",
+ }
+ expected := []string {
+ "{$}",
+ "/{$}",
+ "/",
+ "/abc",
+ "/abc/{$}",
+ "/abc/",
+ "abc/",
+ }
+
+ for i, input := range inputs {
+ g.AssertEqual(adjustPattern(input), expected[i])
+ }
+}
+
+
+func MainTest() {
+ test_adjustPattern()
+}
diff --git a/tests/integration.sh b/tests/integration.sh
new file mode 100755
index 0000000..e0a92dc
--- /dev/null
+++ b/tests/integration.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+
+. tests/lib.sh
+
+
+test_exits_when_upstream_errors() {
+ testing 'exits when upstream errors'
+
+ N="$LINENO"
+ OUT="$(mkstemp)"
+ ERR="$(mkstemp)"
+ trap 'rm -f "$OUT" "$ERR" s1.socket client.txt' EXIT
+
+ rm -f s1.socket
+ ./binder.bin localhost:1234 s1.socket 1>"$OUT" 2>"$ERR" &
+ pid=$!
+ while ! lsof -s TCP:LISTEN -i :1234 > /dev/null; do
+ true
+ done
+
+ echo request | socat tcp-connect:localhost:1234 stdio > client.txt
+ kill $pid
+ wait
+
+ assert_fgrep_stdout 'listen-start'
+ assert_fgrep_stdout 'active-connections'
+ assert_fgrep_stdout 'dial-connection'
+ assert_empty_stderr
+ assert_empty_stream 'client.txt' client.txt
+ rm -f "$OUT" "$ERR" s1.socket client.txt
+
+ test_ok
+}
+
+test_works_from_client_to_server() {
+ testing 'works from client to server'
+
+ N="$LINENO"
+ OUT="$(mkstemp)"
+ ERR="$(mkstemp)"
+ trap 'rm -f "$OUT" "$ERR" s2.socket client.txt server.txt' EXIT
+
+ rm -f s2.socket
+ ./binder.bin localhost:1234 s2.socket 1>"$OUT" 2>"$ERR" &
+ pid=$!
+ while ! lsof -s TCP:LISTEN -i :1234 > /dev/null; do
+ true
+ done
+
+ echo response | socat unix-listen:s2.socket stdio > server.txt &
+ while [ ! -S s2.socket ]; do
+ true
+ done
+
+ echo request | socat tcp-connect:localhost:1234 stdio > client.txt
+ kill $pid
+ wait
+
+ assert_fgrep_stdout 'listen-start'
+ assert_fgrep_stdout 'active-connections'
+ assert_empty_stderr
+ assert_fgrep_stream 'client.txt' client.txt 'response'
+ assert_fgrep_stream 'server.txt' server.txt 'request'
+ rm -f "$OUT" "$ERR" s2.socket client.txt server.txt
+
+ test_ok
+}
+
+
+exit # FIXME
+test_exits_when_upstream_errors
+test_works_from_client_to_server
diff --git a/tests/lib.sh b/tests/lib.sh
new file mode 100644
index 0000000..07ecbef
--- /dev/null
+++ b/tests/lib.sh
@@ -0,0 +1,119 @@
+#!/bin/sh
+
+end="\033[0m"
+red="\033[0;31m"
+green="\033[0;32m"
+yellow="\033[0;33m"
+
+N=
+OUT=
+ERR=
+STATUS=
+
+ERROR() {
+ # shellcheck disable=2059
+ printf "${red}ERROR${end}"
+}
+
+print_debug_info() {
+ # shellcheck disable=2016
+ printf 'LINENO: %s\n$OUT: %s\n$ERR: %s\n' \
+ "$N" "$OUT" "$ERR" >&2
+}
+
+assert_status() {
+ if [ "$STATUS" != "$1" ]; then
+ printf '\n%s: Bad status.\n\nexpected: %s\ngot: %s\n' \
+ "$(ERROR)" "$1" "$STATUS" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_usage() {
+ if ! grep -Fq 'Usage' "$1"; then
+ echo 'Expected to find "Usage" text, it was missing:' >&2
+ cat "$1" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_empty_stream() {
+ if [ -s "$2" ]; then
+ FMT='\n%s: Expected %s (%s) to be empty, but has content:\n%s\n'
+ # shellcheck disable=2059
+ printf "$FMT" \
+ "$(ERROR)" "$1" "$2" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_empty_stdout() {
+ assert_empty_stream STDOUT "$OUT"
+}
+
+assert_empty_stderr() {
+ assert_empty_stream STDERR "$ERR"
+}
+
+assert_stream() {
+ if [ "$(cat "$2")" != "$3" ]; then
+ printf '\n%s: Bad %s (%s)\n\nexpected: %s\ngot: %s\n' \
+ "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_stdout() {
+ assert_stream STDOUT "$OUT" "$1"
+}
+
+assert_stderr() {
+ assert_stream STDERR "$ERR" "$1"
+}
+
+assert_grep_stream() {
+ if ! grep -qE "$3" "$2"; then
+ printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \
+ "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_grep_stdout() {
+ assert_grep_stream STDOUT "$OUT" "$1"
+}
+
+assert_grep_stderr() {
+ assert_grep_stream STDERR "$ERR" "$1"
+}
+
+assert_fgrep_stream() {
+ if ! grep -Fq -- "$3" "$2"; then
+ printf '\n%s: Bad %s (%s)\n\ngrepping: %s\nin:\n%s\n' \
+ "$(ERROR)" "$1" "$2" "$3" "$(cat "$2")" >&2
+ print_debug_info
+ exit 1
+ fi
+}
+
+assert_fgrep_stdout() {
+ assert_fgrep_stream STDOUT "$OUT" "$1"
+}
+
+assert_fgrep_stderr() {
+ assert_fgrep_stream STDERR "$ERR" "$1"
+}
+
+testing() {
+ printf "${yellow}testing${end}: %s..." "$1" >&2
+}
+
+test_ok() {
+ # shellcheck disable=2059
+ printf " ${green}OK${end}.\n" >&2
+}
diff --git a/tests/lib_test.go b/tests/lib_test.go
deleted file mode 100644
index 31259c6..0000000
--- a/tests/lib_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package lib_test
-
-import (
- "testing"
-
- "euandre.org/glaze/src"
-)
-
-func TestPlaceholder(t *testing.T) {
- if (&glaze.PatternPath{}).String() != "FIXME" {
- t.Fail()
- }
-}
diff --git a/tests/main.go b/tests/main.go
new file mode 100644
index 0000000..835ef3a
--- /dev/null
+++ b/tests/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "glaze"
+
+func main() {
+ glaze.MainTest()
+}
diff --git a/tests/resources/index.html b/tests/resources/index.html
new file mode 100644
index 0000000..4a3ce66
--- /dev/null
+++ b/tests/resources/index.html
@@ -0,0 +1,4 @@
+olar
+
+oiwjef
+
diff --git a/tests/resources/link.txt b/tests/resources/link.txt
new file mode 120000
index 0000000..44ab045
--- /dev/null
+++ b/tests/resources/link.txt
@@ -0,0 +1 @@
+original.txt \ No newline at end of file
diff --git a/tests/resources/original.txt b/tests/resources/original.txt
new file mode 100644
index 0000000..f00c965
--- /dev/null
+++ b/tests/resources/original.txt
@@ -0,0 +1,10 @@
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10