Skip to content

Commit

Permalink
Move http from server package to protocol package
Browse files Browse the repository at this point in the history
Protocol package handles the different types of incoming
requests rq accepts. Currently http is the only protocol but this
allows other protocols to use the interface. Ie: Websockets

- Created a payload struct which we use between channels.
- Moved the errorLog logger interface to this package because no other package needs to reuse it.

- Changes to the start method:
- Added a wg which allows us to control the goroutine from the server package.
- Added a slice of renderer channels which we send incoming requests to.
- Added a slice of quit channels to close renderers in the case that the http protocol has a fatal error.
  • Loading branch information
aaronvb committed Jun 14, 2021
1 parent 971f4cd commit fd955af
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 163 deletions.
68 changes: 43 additions & 25 deletions pkg/server/http.go → pkg/protocol/http.go
Original file line number Diff line number Diff line change
@@ -1,61 +1,67 @@
package server
package protocol

import (
"fmt"
"log"
"net/http"
"sync"

"github.com/aaronvb/logparams"
"github.com/aaronvb/logrequest"
"github.com/aaronvb/request_hole/pkg/renderer"
"github.com/gorilla/mux"
"github.com/pterm/pterm"
)

// Http is the protocol for accepting http requests.
type Http struct {
// Port is the port the HTTP server will run on.
Port int

// Addr is the address the HTTP server will bind to.
Addr string

// Port is the port the HTTP server will run on.
Port int

// ResponseCode is the response which out endpoint will return.
// Default is 200 if no response code is passed.
ResponseCode int

// Output is the Renderer interface.
Output renderer.Renderer

// LogOutput
LogOutput renderer.Renderer

// Determines if header details should be shown with the request
Details bool
// rendererChannel is the channel which we send a RequestPayload to when
// receiving an incoming request to the Http protocol.
rendererChannels []chan RequestPayload
}

// Start will start the HTTP server.
func (s *Http) Start() {
s.Output.Start()
s.LogOutput.Start()

//
// Sets the channel on our struct so that incoming requests can be sent over it.
//
// In the case that we cannot start this server, we send a signal to our quit channel
// to close renderers.
func (s *Http) Start(wg *sync.WaitGroup, c []chan RequestPayload, quits []chan int) {
addr := fmt.Sprintf("%s:%d", s.Addr, s.Port)
errorLog := log.New(&renderer.PrinterLog{Prefix: pterm.Error}, "", 0)
errorLog := log.New(&httpErrorLog{}, "", 0)

srv := &http.Server{
Addr: addr,
ErrorLog: errorLog,
Handler: s.routes(),
}

s.rendererChannels = c

defer wg.Done()

err := srv.ListenAndServe()
s.Output.Fatal(err)
str := pterm.Error.WithShowLineNumber(false).Sprintf("%s\n", err)
pterm.Printo(str) // Overwrite last line

for _, quit := range quits {
quit <- 1
}
}

// routes handles the routes for our HTTP server and currently accepts any path.
func (s *Http) routes() http.Handler {
r := mux.NewRouter()
r.PathPrefix("/").HandlerFunc(s.defaultHandler)

r.Use(s.logRequest)

return r
Expand All @@ -75,12 +81,24 @@ func (s *Http) logRequest(next http.Handler) http.Handler {
fields := lr.ToFields()
params := logparams.LogParams{Request: r, HidePrefix: true}

s.Output.IncomingRequest(fields, params.ToString())
s.LogOutput.IncomingRequest(fields, params.ToString())
req := RequestPayload{
Fields: fields,
Params: params.ToString(),
Headers: r.Header,
}

if s.Details {
s.Output.IncomingRequestHeaders(r.Header)
s.LogOutput.IncomingRequestHeaders(r.Header)
for _, rendererChannel := range s.rendererChannels {
rendererChannel <- req
}
})
}

// httpErrorLog implements the logger interface.
type httpErrorLog struct{}

// Write let's us override the logger required for http errors and
// prints to the terminal using pterm.
func (e *httpErrorLog) Write(b []byte) (n int, err error) {
pterm.Error.WithShowLineNumber(false).Println(string(b))
return len(b), nil
}
187 changes: 187 additions & 0 deletions pkg/protocol/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package protocol

import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)

func TestResponseCodeFlag(t *testing.T) {
tests := []int{200, 404, 201, 500}

for _, respCode := range tests {
httpServer := Http{ResponseCode: respCode}
srv := httptest.NewServer(httpServer.routes())
req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
if err != nil {
t.Error(err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
}

if resp.StatusCode != respCode {
t.Errorf("Expected %d, got %d", respCode, resp.StatusCode)
}
resp.Body.Close()
srv.Close()
}
}

func TestLogRequestOneRenderer(t *testing.T) {
testTable := []struct {
method string
path string
body string
expectedParams string
headerKey string
headerValue string
}{
{
http.MethodGet,
"/foo",
"",
"",
"Foo",
"Bar",
},
{
http.MethodPost,
"/foo/bar",
"{\"foo\": \"bar\"}",
"{\"foo\" => \"bar\"}",
"Content-Type",
"application/json",
},
{
http.MethodDelete,
"/foo/1",
"",
"",
"Bearer",
"hello!",
},
{
http.MethodGet,
"/foo/bar?hello=world",
"",
"{\"hello\" => \"world\"}",
"Content-Type",
"application/json!",
},
}

rpChannel := make(chan RequestPayload, len(testTable))
httpServer := Http{ResponseCode: 200, rendererChannels: []chan RequestPayload{rpChannel}}
srv := httptest.NewServer(httpServer.routes())
defer srv.Close()

for _, test := range testTable {
b := bytes.NewBuffer([]byte(test.body))
req, err := http.NewRequest(test.method, srv.URL+test.path, b)

if test.headerKey != "" {
req.Header.Set(test.headerKey, test.headerValue)
}

if err != nil {
t.Error(err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
}
resp.Body.Close()

rp := <-rpChannel

if rp.Fields.Method != test.method {
t.Errorf("Expected %s, got %s", test.method, rp.Fields.Method)
}

if rp.Fields.Url != test.path {
t.Errorf("Expected %s, got %s", test.path, rp.Fields.Url)
}

if rp.Params != test.expectedParams {
t.Errorf("Expected %s, got %s", test.expectedParams, rp.Params)
}

expectedHeaderValue := rp.Headers[test.headerKey][0]
if expectedHeaderValue != test.headerValue {
t.Errorf("Expected %s, got %s", expectedHeaderValue, test.headerValue)
}
}
}

func TestLogRequestManyRenderers(t *testing.T) {
testTable := []struct {
method string
path string
Body string
expectedParams string
}{
{http.MethodGet, "/foo", "", ""},
{http.MethodPost, "/foo/bar", "{\"foo\": \"bar\"}", "{\"foo\" => \"bar\"}"},
{http.MethodDelete, "/foo/1", "", ""},
{http.MethodGet, "/foo/bar?hello=world", "", "{\"hello\" => \"world\"}"},
}

rpChannelA := make(chan RequestPayload, len(testTable))
rpChannelB := make(chan RequestPayload, len(testTable))
httpServer := Http{
ResponseCode: 200,
rendererChannels: []chan RequestPayload{rpChannelA, rpChannelB}}
srv := httptest.NewServer(httpServer.routes())
defer srv.Close()

for _, test := range testTable {
b := bytes.NewBuffer([]byte(test.Body))
req, err := http.NewRequest(test.method, srv.URL+test.path, b)

if test.Body != "" {
req.Header.Set("Content-Type", "application/json")
}

if err != nil {
t.Error(err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Error(err)
}
resp.Body.Close()

rpA := <-rpChannelA
rpB := <-rpChannelB

if rpA.Fields.Method != test.method {
t.Errorf("Expected %s, got %s", test.method, rpA.Fields.Method)
}

if rpA.Fields.Url != test.path {
t.Errorf("Expected %s, got %s", test.path, rpA.Fields.Url)
}

if rpA.Params != test.expectedParams {
t.Errorf("Expected %s, got %s", test.expectedParams, rpA.Params)
}

if rpB.Fields.Method != test.method {
t.Errorf("Expected %s, got %s", test.method, rpB.Fields.Method)
}

if rpB.Fields.Url != test.path {
t.Errorf("Expected %s, got %s", test.path, rpB.Fields.Url)
}

if rpB.Params != test.expectedParams {
t.Errorf("Expected %s, got %s", test.expectedParams, rpB.Params)
}
}
}
23 changes: 23 additions & 0 deletions pkg/protocol/protocol.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package protocol

import (
"sync"

"github.com/aaronvb/logrequest"
)

// Protocol is the interface for the servers that accept incoming requests.
// Incoming requests are then sent to the renderers through the RequestPayload channel.
// If a protocol closes(ie: from and error), we use the second channel which is used to
// send an int(1 signals quit).
type Protocol interface {
Start(*sync.WaitGroup, []chan RequestPayload, []chan int)
}

// RequestPayload is the request payload we receive from an incoming request that we use with
// the renderers.
type RequestPayload struct {
Fields logrequest.RequestFields `json:"fields"`
Headers map[string][]string `json:"headers"`
Params string `json:"params"`
}
Loading

0 comments on commit fd955af

Please sign in to comment.