Skip to content

Commit

Permalink
Merge pull request #16 from jmalloc/sse
Browse files Browse the repository at this point in the history
Add a route that sends "server-sent events".
  • Loading branch information
jmalloc authored Aug 16, 2021
2 parents 1a1b959 + 18b9972 commit 25d2847
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog], and this project adheres to
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html

## [Unreleased]

- Add the `/.see` route

## [0.2.0] - 2021-06-03

- Add support for logging HTTP headers to stdout (thanks [@arulrajnet])
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# Echo Server

A very simple HTTP echo server with support for websockets.
A very simple HTTP echo server with support for websockets and server-sent
events (SSE).

The server is designed for testing HTTP proxies and clients. It echoes
information about HTTP request headers and bodies back to the client.

## Behavior

- Any messages sent from a websocket client are echoed
- Visit `/.ws` for a basic UI to connect and send websocket messages
- Requests to any other URL will return the request headers and body
- Any messages sent from a websocket client are echoed as a websocket message
- Visit `/.ws` in a browser for a basic UI to connect and send websocket messages
- Request `/.sse` to receive the echo response via server-sent events
- Request any other URL to receive the echo response in plain text

## Configuration

Expand Down
112 changes: 106 additions & 6 deletions cmd/echo-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/gorilla/websocket"
"golang.org/x/net/http2"
Expand Down Expand Up @@ -41,6 +44,7 @@ var upgrader = websocket.Upgrader{
}

func handler(wr http.ResponseWriter, req *http.Request) {
defer req.Body.Close()

if os.Getenv("LOG_HTTP_BODY") != "" || os.Getenv("LOG_HTTP_HEADERS") != "" {
fmt.Printf("-------- %s | %s %s\n", req.RemoteAddr, req.Method, req.URL)
Expand Down Expand Up @@ -80,6 +84,8 @@ func handler(wr http.ResponseWriter, req *http.Request) {
wr.Header().Add("Content-Type", "text/html")
wr.WriteHeader(200)
io.WriteString(wr, websocketHTML) // nolint:errcheck
} else if req.URL.Path == "/.sse" {
serveSSE(wr, req)
} else {
serveHTTP(wr, req)
}
Expand Down Expand Up @@ -143,15 +149,109 @@ func serveHTTP(wr http.ResponseWriter, req *http.Request) {
fmt.Fprintf(wr, "Server hostname unknown: %s\n\n", err.Error())
}

fmt.Fprintf(wr, "%s %s %s\n", req.Proto, req.Method, req.URL)
fmt.Fprintln(wr, "")
fmt.Fprintf(wr, "Host: %s\n", req.Host)
writeRequest(wr, req)
}

func serveSSE(wr http.ResponseWriter, req *http.Request) {
if _, ok := wr.(http.Flusher); !ok {
http.Error(wr, "Streaming unsupported!", http.StatusInternalServerError)
return
}

var echo strings.Builder
writeRequest(&echo, req)

wr.Header().Set("Content-Type", "text/event-stream")
wr.Header().Set("Cache-Control", "no-cache")
wr.Header().Set("Connection", "keep-alive")
wr.Header().Set("Access-Control-Allow-Origin", "*")

var id int

// Write an event about the server that is serving this request.
if host, err := os.Hostname(); err == nil {
writeSSE(
wr,
req,
&id,
"server",
host,
)
}

// Write an event that echoes back the request.
writeSSE(
wr,
req,
&id,
"request",
echo.String(),
)

// Then send a counter event every second.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()

for {
select {
case <-req.Context().Done():
return
case t := <-ticker.C:
writeSSE(
wr,
req,
&id,
"time",
t.Format(time.RFC3339),
)
}
}
}

// writeSSE sends a server-sent event and logs it to the console.
func writeSSE(
wr http.ResponseWriter,
req *http.Request,
id *int,
event, data string,
) {
*id++
writeSSEField(wr, req, "event", event)
writeSSEField(wr, req, "data", data)
writeSSEField(wr, req, "id", strconv.Itoa(*id))
fmt.Fprintf(wr, "\n")
wr.(http.Flusher).Flush()
}

// writeSSEField sends a single field within an event.
func writeSSEField(
wr http.ResponseWriter,
req *http.Request,
k, v string,
) {
for _, line := range strings.Split(v, "\n") {
fmt.Fprintf(wr, "%s: %s\n", k, line)
fmt.Printf("%s | sse | %s: %s\n", req.RemoteAddr, k, line)
}
}

// writeRequest writes request headers to w.
func writeRequest(w io.Writer, req *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", req.Proto, req.Method, req.URL)
fmt.Fprintln(w, "")

fmt.Fprintf(w, "Host: %s\n", req.Host)
for key, values := range req.Header {
for _, value := range values {
fmt.Fprintf(wr, "%s: %s\n", key, value)
fmt.Fprintf(w, "%s: %s\n", key, value)
}
}

fmt.Fprintln(wr, "")
io.Copy(wr, req.Body) // nolint:errcheck
var body bytes.Buffer
io.Copy(&body, req.Body) // nolint:errcheck

if body.Len() > 0 {
fmt.Fprintln(w, "")
body.WriteTo(w) // nolint:errcheck
}
}

0 comments on commit 25d2847

Please sign in to comment.