httpw

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2025 License: MIT Imports: 13 Imported by: 0

README

HTTP wrecker

Purpose

HTTP wrecker which provides an ability to interrupt execution of requests to an upstream server

Usage

Example:

package main

import (
    "bytes"
    "context"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/url"
    "time"

    "github.com/akramarenkov/wrecker/httpw"
)

func main() {
    message := []byte("example message")

    upstreamListener, err := net.Listen("tcp", "127.0.0.1:")
    if err != nil {
        panic(err)
    }

    var upstreamRouter http.ServeMux

    upstreamRouter.HandleFunc(
        "/api",
        func(w http.ResponseWriter, _ *http.Request) {
            _, _ = w.Write(message)
        },
    )

    upstreamRouter.HandleFunc(
        "/forbidden",
        func(w http.ResponseWriter, _ *http.Request) {
            _, _ = w.Write(message)
        },
    )

    upstreamServer := &http.Server{
        Handler:     &upstreamRouter,
        ReadTimeout: 5 * time.Second,
    }

    upstreamErr := make(chan error)
    defer close(upstreamErr)

    defer func() {
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        if err := upstreamServer.Shutdown(ctx); err != nil {
            fmt.Println("Upstream server shutdown error:", err)
        }

        if err := <-upstreamErr; !errors.Is(err, http.ErrServerClosed) {
            fmt.Println("Upstream server has terminated abnormally:", err)
        }
    }()

    go func() {
        upstreamErr <- upstreamServer.Serve(upstreamListener)
    }()

    upstreamURL := url.URL{
        Scheme: "http",
        Host:   upstreamListener.Addr().String(),
    }

    opts := httpw.Opts{
        Network:  "tcp",
        Address:  "127.0.0.1:",
        Upstream: upstreamURL.String(),
        Blockers: []httpw.Blocker{
            func(req *http.Request) bool {
                return req.URL.Path == "/forbidden"
            },
        },
    }

    wrecker, err := httpw.New(opts)
    if err != nil {
        panic(err)
    }

    defer func() {
        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        if err := wrecker.Shutdown(ctx); err != nil {
            fmt.Println("Wrecker shutdown error:", err)
        }

        if err := <-wrecker.Err(); !errors.Is(err, http.ErrServerClosed) {
            fmt.Println("Wrecker has terminated abnormally:", err)
        }
    }()

    apiURL := url.URL{
        Scheme: "http",
        Host:   wrecker.Addr().String(),
        Path:   "/api",
    }

    resp, err := http.Get(apiURL.String())
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    received, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    fmt.Println(
        "Is message sent by server equal to message received by client:",
        bytes.Equal(received, message),
    )

    forbiddenURL := url.URL{
        Scheme: "http",
        Host:   wrecker.Addr().String(),
        Path:   "/forbidden",
    }

    resp, err = http.Get(forbiddenURL.String())
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    fmt.Println(
        "Is the request to a forbidden path aborted:",
        resp.StatusCode == http.StatusForbidden,
    )
    // Output:
    // Is message sent by server equal to message received by client: true
    // Is the request to a forbidden path aborted: true
}

Documentation

Overview

HTTP wrecker which provides an ability to interrupt execution of requests to an upstream server.

Index

Examples

Constants

View Source
const (
	UnixSchemeHTTP  = "http+unix"
	UnixSchemeHTTPS = "https+unix"
)
View Source
const DefaultReadTimeout = 10 * time.Second

Variables

This section is empty.

Functions

This section is empty.

Types

type Blocker added in v0.5.0

type Blocker func(*http.Request) bool

Decides whether to return an error or redirect a request to an upstream server.

If it returns true, an error will be sent to a sender and a request will not be forwarded to an upstream server.

Each blocker receives a copy of the request body, which it can read independently from other blockers and from the upstream server.

type Handler added in v0.4.0

type Handler struct {
	// contains filtered or unexported fields
}

HTTP wrecker in the form of http.Handler.

Example
package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/akramarenkov/wrecker/httpw"
)

func main() {
	message := []byte("example message")

	upstreamListener, err := net.Listen("tcp", "127.0.0.1:")
	if err != nil {
		panic(err)
	}

	wreckerListener, err := net.Listen("tcp", "127.0.0.1:")
	if err != nil {
		panic(err)
	}

	upstreamURL := url.URL{
		Scheme: "http",
		Host:   upstreamListener.Addr().String(),
	}

	blocker := func(req *http.Request) bool {
		return req.URL.Path == "/forbidden"
	}

	opts := httpw.HandlerOpts{
		Upstream: upstreamURL.String(),
		Blockers: []httpw.Blocker{blocker},
	}

	wrecker, err := httpw.NewHandler(opts)
	if err != nil {
		panic(err)
	}

	var upstreamRouter http.ServeMux

	upstreamRouter.HandleFunc(
		"/api",
		func(w http.ResponseWriter, _ *http.Request) {
			_, _ = w.Write(message)
		},
	)

	upstreamRouter.HandleFunc(
		"/forbidden",
		func(w http.ResponseWriter, _ *http.Request) {
			_, _ = w.Write(message)
		},
	)

	upstreamServer := &http.Server{
		Handler:     &upstreamRouter,
		ReadTimeout: 5 * time.Second,
	}

	wreckerServer := &http.Server{
		Handler:     wrecker,
		ReadTimeout: 5 * time.Second,
	}

	serversErrs := make(chan error)
	defer close(serversErrs)

	defer func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := upstreamServer.Shutdown(ctx); err != nil {
			fmt.Println("Upstream server shutdown error:", err)
		}

		if err := <-serversErrs; !errors.Is(err, http.ErrServerClosed) {
			fmt.Println("Upstream server has terminated abnormally:", err)
		}

		if err := wreckerServer.Shutdown(ctx); err != nil {
			fmt.Println("Wrecker server shutdown error:", err)
		}

		if err := <-serversErrs; !errors.Is(err, http.ErrServerClosed) {
			fmt.Println("Wrecker server has terminated abnormally:", err)
		}
	}()

	go func() {
		serversErrs <- upstreamServer.Serve(upstreamListener)
	}()

	go func() {
		serversErrs <- wreckerServer.Serve(wreckerListener)
	}()

	apiURL := url.URL{
		Scheme: "http",
		Host:   wreckerListener.Addr().String(),
		Path:   "/api",
	}

	resp, err := http.Get(apiURL.String())
	if err != nil {
		panic(err)
	}

	defer resp.Body.Close()

	received, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	fmt.Println(
		"Is message sent by server equal to message received by client:",
		bytes.Equal(received, message),
	)

	forbiddenURL := url.URL{
		Scheme: "http",
		Host:   wreckerListener.Addr().String(),
		Path:   "/forbidden",
	}

	resp, err = http.Get(forbiddenURL.String())
	if err != nil {
		panic(err)
	}

	defer resp.Body.Close()

	fmt.Println(
		"Is the request to a forbidden path aborted:",
		resp.StatusCode == http.StatusForbidden,
	)
}
Output:

Is message sent by server equal to message received by client: true
Is the request to a forbidden path aborted: true

func NewHandler added in v0.6.0

func NewHandler(opts HandlerOpts) (*Handler, error)

Creates a new HTTP wrecker in the form of http.Handler.

func (*Handler) ServeHTTP added in v0.4.0

func (hdl *Handler) ServeHTTP(wrt http.ResponseWriter, req *http.Request)

Implements the http.Handler interface.

type HandlerOpts added in v0.6.0

type HandlerOpts struct {
	// URL of upstream server. Required parameter
	Upstream string

	// List of the blockers
	Blockers []Blocker

	// List of the spoilers
	Spoilers []Spoiler

	// Transport for proxied requests
	ProxyTransport http.RoundTripper
}

type Opts added in v0.4.0

type Opts struct {
	// Network to listen, as in [net.Listen]. Required parameter
	Network string

	// Address to listen, as in [net.Listen]. Required parameter
	Address string

	// URL of upstream server. Required parameter
	Upstream string

	// List of the blockers
	Blockers []Blocker

	// List of the spoilers
	Spoilers []Spoiler

	// Transport for proxied requests
	ProxyTransport http.RoundTripper

	// Parameters of server. Addr and Handler fields are ignored
	Server *http.Server
}

Options of the created instance of the HTTP wrecker with an HTTP server inside.

type Response added in v0.7.0

type Response struct {
	// Body of a response
	Body []byte

	// Header maps of a response
	Header http.Header

	// Request that was sent to obtain this Response
	Request *http.Request

	// Status code of a response
	StatusCode int
}

Data of response from upstream server.

type Spoiler added in v0.6.0

type Spoiler func(*Response) bool

Decides whether to return an error or pass response to an sender.

If it returns true, an error will be sent to a sender without response headers and body.

Each spoiler receives a copy of response headers and body, also a copy of the request body.

type Wrecker

type Wrecker struct {
	// contains filtered or unexported fields
}

HTTP wrecker with an HTTP server inside.

Example
package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/akramarenkov/wrecker/httpw"
)

func main() {
	message := []byte("example message")

	upstreamListener, err := net.Listen("tcp", "127.0.0.1:")
	if err != nil {
		panic(err)
	}

	var upstreamRouter http.ServeMux

	upstreamRouter.HandleFunc(
		"/api",
		func(w http.ResponseWriter, _ *http.Request) {
			_, _ = w.Write(message)
		},
	)

	upstreamRouter.HandleFunc(
		"/forbidden",
		func(w http.ResponseWriter, _ *http.Request) {
			_, _ = w.Write(message)
		},
	)

	upstreamServer := &http.Server{
		Handler:     &upstreamRouter,
		ReadTimeout: 5 * time.Second,
	}

	upstreamErr := make(chan error)
	defer close(upstreamErr)

	defer func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := upstreamServer.Shutdown(ctx); err != nil {
			fmt.Println("Upstream server shutdown error:", err)
		}

		if err := <-upstreamErr; !errors.Is(err, http.ErrServerClosed) {
			fmt.Println("Upstream server has terminated abnormally:", err)
		}
	}()

	go func() {
		upstreamErr <- upstreamServer.Serve(upstreamListener)
	}()

	upstreamURL := url.URL{
		Scheme: "http",
		Host:   upstreamListener.Addr().String(),
	}

	opts := httpw.Opts{
		Network:  "tcp",
		Address:  "127.0.0.1:",
		Upstream: upstreamURL.String(),
		Blockers: []httpw.Blocker{
			func(req *http.Request) bool {
				return req.URL.Path == "/forbidden"
			},
		},
	}

	wrecker, err := httpw.New(opts)
	if err != nil {
		panic(err)
	}

	defer func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		if err := wrecker.Shutdown(ctx); err != nil {
			fmt.Println("Wrecker shutdown error:", err)
		}

		if err := <-wrecker.Err(); !errors.Is(err, http.ErrServerClosed) {
			fmt.Println("Wrecker has terminated abnormally:", err)
		}
	}()

	apiURL := url.URL{
		Scheme: "http",
		Host:   wrecker.Addr().String(),
		Path:   "/api",
	}

	resp, err := http.Get(apiURL.String())
	if err != nil {
		panic(err)
	}

	defer resp.Body.Close()

	received, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	fmt.Println(
		"Is message sent by server equal to message received by client:",
		bytes.Equal(received, message),
	)

	forbiddenURL := url.URL{
		Scheme: "http",
		Host:   wrecker.Addr().String(),
		Path:   "/forbidden",
	}

	resp, err = http.Get(forbiddenURL.String())
	if err != nil {
		panic(err)
	}

	defer resp.Body.Close()

	fmt.Println(
		"Is the request to a forbidden path aborted:",
		resp.StatusCode == http.StatusForbidden,
	)
}
Output:

Is message sent by server equal to message received by client: true
Is the request to a forbidden path aborted: true

func New

func New(opts Opts) (*Wrecker, error)

Creates and runs an HTTP wrecker with an HTTP server inside.

func (*Wrecker) Addr added in v0.4.0

func (wrc *Wrecker) Addr() net.Addr

Returns the address on which the wrecker is listening.

func (*Wrecker) Close added in v0.4.0

func (wrc *Wrecker) Close() error

Immediately closes all listeners and connections of the wrecker server, simply calling http.Server.Close.

func (*Wrecker) Err added in v0.4.0

func (wrc *Wrecker) Err() <-chan error

Returns a channel with errors occurring in the wrecker server.

When the wrecker server is terminated by the Wrecker.Shutdown or Wrecker.Close methods, http.ErrServerClosed is returned.

func (*Wrecker) Shutdown added in v0.4.0

func (wrc *Wrecker) Shutdown(ctx context.Context) error

Gracefully shuts down the wrecker server, simply calling http.Server.Shutdown.

Directories

Path Synopsis
internal
gatherer
Internal package with Gatherer that gathers response body, stores headers and response status code.
Internal package with Gatherer that gathers response body, stores headers and response status code.
unhijacked
Internal package with wrapper over http.ResponseWriter that does not implement the http.Hijacker interface.
Internal package with wrapper over http.ResponseWriter that does not implement the http.Hijacker interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL