Skip to content

Instantly share code, notes, and snippets.

@aryszka
Last active June 10, 2019 19:15
Show Gist options
  • Save aryszka/c7e6e60c576709bce385b8e6edd20c22 to your computer and use it in GitHub Desktop.
Save aryszka/c7e6e60c576709bce385b8e6edd20c22 to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
)
const escapeLength = 3 // always, because we only handle the below cases
const (
semicolon = ';'
slash = '/'
questionMark = '?'
colon = ':'
at = '@'
and = '&'
eq = '='
plus = '+'
dollar = '$'
comma = ','
)
// https://tools.ietf.org/html/rfc2396#section-2.2
func unescape(seq []byte) (byte, bool) {
switch string(seq) {
case "%3B", "%3b":
return semicolon, true
case "%2F", "%2f":
return slash, true
case "%3F", "%3f":
return questionMark, true
case "%3A", "%3a":
return colon, true
case "%40":
return at, true
case "%26":
return and, true
case "%3D", "%3d":
return eq, true
case "%2B", "%2b":
return plus, true
case "%24":
return dollar, true
case "%2C", "%2c":
return comma, true
default:
return 0, false
}
}
func patchPath(r *http.Request) {
parsed, raw := []byte(r.URL.Path), []byte(r.URL.RawPath)
if q := bytes.IndexByte(raw, '?'); q >= 0 {
raw = raw[:q]
}
patched := make([]byte, 0, len(raw))
var (
escape bool
seq []byte
unescaped byte
handled bool
needsPatch bool
modified bool
pi int
)
for i := 0; i < len(raw); i++ {
c := raw[i]
escape = c == '%'
modified = !escape && (pi >= len(parsed) || parsed[pi] != c)
if modified {
needsPatch = false
break
}
if !escape {
patched = append(patched, parsed[pi])
pi++
continue
}
if len(raw) < i+escapeLength {
needsPatch = false
break
}
seq = raw[i : i+escapeLength]
i += escapeLength - 1
unescaped, handled = unescape(seq)
if !handled {
patched = append(patched, parsed[pi])
pi++
continue
}
modified = parsed[pi] != unescaped
if modified {
needsPatch = false
break
}
patched = append(patched, seq...)
needsPatch = true
pi++
}
if !needsPatch {
return
}
modified = pi < len(parsed)
if modified {
return
}
r.URL.Path = string(patched)
}
func serve(addresses ...string) {
if len(addresses) == 0 {
return
}
address, addresses := addresses[0], addresses[1:]
var forward string
if len(addresses) > 0 {
forward = addresses[0]
}
go serve(addresses...)
if err := http.ListenAndServe(address, http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
log.Println(address, "raw: ", r.RequestURI)
log.Println(address, "raw path:", r.URL.RawPath)
log.Println(address, "parsed: ", r.URL.Path)
if forward == "" {
return
}
patchPath(r)
rsp, err := http.Get(
fmt.Sprintf(
"http://localhost%s%s?%s",
forward, r.URL.Path, r.URL.RawQuery,
),
)
if err != nil {
log.Fatalln(err)
}
if _, err := ioutil.ReadAll(rsp.Body); err != nil {
log.Fatalln(err)
}
rsp.Body.Close()
})); err != nil {
log.Fatalln(err)
}
}
func main() {
go serve(":8080", ":8081")
select {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment