paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

upstream_go.go (6033B)


      1 /*
      2   This file is part of Paivana.
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   Paivana is free software; you can redistribute it and/or
      6   modify it under the terms of the GNU Affero General Public License
      7   as published by the Free Software Foundation; either version
      8   3, or (at your option) any later version.
      9 
     10   Paivana is distributed in the hope that it will be useful, but
     11   WITHOUT ANY WARRANTY; without even the implied warranty of
     12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13   GNU Affero General Public License for more details.
     14 
     15   You should have received a copy of the GNU Affero General Public
     16   License along with Paivana; see the file COPYING.  If not,
     17   write to the Free Software Foundation, Inc., 51 Franklin
     18   Street, Fifth Floor, Boston, MA 02110-1301, USA.
     19 */
     20 
     21 // upstream_go: Go-based upstream HTTP server used by the
     22 // paivana reverse-proxy tests.  Implements the same small set
     23 // of canned endpoints as upstream_mhd.c.
     24 
     25 package main
     26 
     27 import (
     28 	"fmt"
     29 	"io"
     30 	"net/http"
     31 	"os"
     32 	"sort"
     33 	"strconv"
     34 	"strings"
     35 	"time"
     36 )
     37 
     38 const upstreamName = "go"
     39 
     40 func setHeaders(w http.ResponseWriter) {
     41 	w.Header().Set("X-Upstream", upstreamName)
     42 }
     43 
     44 func hello(w http.ResponseWriter, r *http.Request) {
     45 	setHeaders(w)
     46 	w.Header().Set("Content-Type", "text/plain")
     47 	if r.Method == http.MethodHead {
     48 		w.WriteHeader(200)
     49 		return
     50 	}
     51 	fmt.Fprintf(w, "Hello from %s\n", upstreamName)
     52 }
     53 
     54 func status(w http.ResponseWriter, r *http.Request) {
     55 	setHeaders(w)
     56 	path := strings.TrimPrefix(r.URL.Path, "/status/")
     57 	code, err := strconv.Atoi(path)
     58 	if err != nil || code < 100 || code > 599 {
     59 		code = 500
     60 	}
     61 	w.Header().Set("Content-Type", "text/plain")
     62 	w.WriteHeader(code)
     63 	fmt.Fprintf(w, "status %d\n", code)
     64 }
     65 
     66 func large(w http.ResponseWriter, r *http.Request) {
     67 	setHeaders(w)
     68 	path := strings.TrimPrefix(r.URL.Path, "/large/")
     69 	n, err := strconv.Atoi(path)
     70 	if err != nil || n < 0 {
     71 		n = 0
     72 	}
     73 	if n > 10*1024*1024 {
     74 		n = 10 * 1024 * 1024
     75 	}
     76 	buf := make([]byte, n)
     77 	for i := 0; i < n; i++ {
     78 		buf[i] = byte('A' + i%26)
     79 	}
     80 	w.Header().Set("Content-Type", "application/octet-stream")
     81 	w.WriteHeader(200)
     82 	w.Write(buf)
     83 }
     84 
     85 func slow(w http.ResponseWriter, r *http.Request) {
     86 	setHeaders(w)
     87 	path := strings.TrimPrefix(r.URL.Path, "/slow/")
     88 	ms, err := strconv.Atoi(path)
     89 	if err != nil || ms < 0 {
     90 		ms = 0
     91 	}
     92 	if ms > 30000 {
     93 		ms = 30000
     94 	}
     95 	time.Sleep(time.Duration(ms) * time.Millisecond)
     96 	w.Header().Set("Content-Type", "text/plain")
     97 	w.WriteHeader(200)
     98 	fmt.Fprint(w, "slept\n")
     99 }
    100 
    101 func echo(w http.ResponseWriter, r *http.Request) {
    102 	setHeaders(w)
    103 	w.Header().Set("Content-Type", "application/octet-stream")
    104 	body, _ := io.ReadAll(r.Body)
    105 	w.WriteHeader(200)
    106 	w.Write(body)
    107 }
    108 
    109 func upload(w http.ResponseWriter, r *http.Request, label string) {
    110 	setHeaders(w)
    111 	body, _ := io.ReadAll(r.Body)
    112 	w.Header().Set("Content-Type", "text/plain")
    113 	w.WriteHeader(200)
    114 	fmt.Fprintf(w, "%s %d bytes\n", label, len(body))
    115 }
    116 
    117 func echoHeaders(w http.ResponseWriter, r *http.Request) {
    118 	setHeaders(w)
    119 	var keys []string
    120 	for k := range r.Header {
    121 		keys = append(keys, k)
    122 	}
    123 	sort.Strings(keys)
    124 	w.Header().Set("Content-Type", "text/plain")
    125 	w.WriteHeader(200)
    126 	for _, k := range keys {
    127 		for _, v := range r.Header[k] {
    128 			fmt.Fprintf(w, "%s: %s\n", k, v)
    129 		}
    130 	}
    131 	// Host isn't in r.Header
    132 	if r.Host != "" {
    133 		fmt.Fprintf(w, "Host: %s\n", r.Host)
    134 	}
    135 }
    136 
    137 // connResponse names two of its own response headers in Connection;
    138 // a conforming proxy must strip both (RFC 9110 ยง7.6.1) but keep
    139 // X-Keep-Resp.
    140 func connResponse(w http.ResponseWriter, r *http.Request) {
    141 	setHeaders(w)
    142 	w.Header().Set("X-Hop-Before", "must-not-leak")
    143 	w.Header().Set("Connection", "X-Hop-Before, X-Hop-After")
    144 	w.Header().Set("X-Hop-After", "must-not-leak")
    145 	w.Header().Set("X-Keep-Resp", "survivor")
    146 	w.Header().Set("Content-Type", "text/plain")
    147 	w.WriteHeader(200)
    148 	fmt.Fprint(w, "conn\n")
    149 }
    150 
    151 func deleteItem(w http.ResponseWriter, r *http.Request) {
    152 	setHeaders(w)
    153 	w.WriteHeader(204)
    154 }
    155 
    156 func options(w http.ResponseWriter, r *http.Request) {
    157 	setHeaders(w)
    158 	w.Header().Set("Allow", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
    159 	w.WriteHeader(204)
    160 }
    161 
    162 func route(w http.ResponseWriter, r *http.Request) {
    163 	if r.Method == http.MethodOptions {
    164 		options(w, r)
    165 		return
    166 	}
    167 	switch {
    168 	case r.URL.Path == "/hello":
    169 		hello(w, r)
    170 	case strings.HasPrefix(r.URL.Path, "/status/"):
    171 		status(w, r)
    172 	case strings.HasPrefix(r.URL.Path, "/large/"):
    173 		large(w, r)
    174 	case strings.HasPrefix(r.URL.Path, "/slow/"):
    175 		slow(w, r)
    176 	case r.URL.Path == "/echo" && r.Method == http.MethodPost:
    177 		echo(w, r)
    178 	case r.URL.Path == "/upload" && r.Method == http.MethodPost:
    179 		upload(w, r, "Received")
    180 	case r.URL.Path == "/put" && r.Method == http.MethodPut:
    181 		upload(w, r, "PUT received")
    182 	case r.URL.Path == "/patch" && r.Method == http.MethodPatch:
    183 		upload(w, r, "PATCH received")
    184 	case strings.HasPrefix(r.URL.Path, "/item") && r.Method == http.MethodDelete:
    185 		deleteItem(w, r)
    186 	case r.URL.Path == "/echo-headers":
    187 		echoHeaders(w, r)
    188 	case r.URL.Path == "/conn-response":
    189 		connResponse(w, r)
    190 	default:
    191 		w.Header().Set("Content-Type", "text/plain")
    192 		w.WriteHeader(404)
    193 		fmt.Fprint(w, "not found\n")
    194 	}
    195 }
    196 
    197 func main() {
    198 	port := 8402
    199 	if len(os.Args) > 1 {
    200 		// ":0" would bind an ephemeral port while the driver waits
    201 		// for the one it asked for; ":garbage" would fail five
    202 		// seconds into that wait, blaming the wrong thing.
    203 		n, err := strconv.Atoi(os.Args[1])
    204 		if err != nil || n < 1 || n > 65535 {
    205 			fmt.Fprintf(os.Stderr, "invalid port %q\n", os.Args[1])
    206 			os.Exit(1)
    207 		}
    208 		port = n
    209 	}
    210 	// Loopback only: this server echoes an arbitrary POST body back
    211 	// and hands out 10 MiB on request, and has no business being
    212 	// reachable from the network for the duration of `make check'.
    213 	srv := &http.Server{
    214 		Addr:    fmt.Sprintf("127.0.0.1:%d", port),
    215 		Handler: http.HandlerFunc(route),
    216 	}
    217 	fmt.Fprintf(os.Stderr, "upstream_go listening on port %d\n", port)
    218 	if err := srv.ListenAndServe(); err != nil {
    219 		fmt.Fprintln(os.Stderr, err)
    220 		os.Exit(1)
    221 	}
    222 }