how to make a proxy by golang - go

I'm trying to make a proxy by golang.
The origin version is written by lua, nginx like this:
location / {
keepalive_timeout 3600s;
keepalive_requests 30000;
rewrite_by_lua_file ./test.lua;
proxy_pass http://www.example.com/bd/news/home;
}
and lua file like this:
local req_params = ngx.req.get_uri_args()
local args = {
media = 24,
submedia = 46,
os = req_params.os,
osv = req_params.osv,
make = req_params.make,
model = req_params.model,
devicetype = req_params.devicetype,
conn = req_params.conn,
carrier = req_params.carrier,
sw = req_params.w,
sh = req_params.h,
}
if tonumber(req_params.os) == 1 then
args.imei = req_params.imei
args.adid = req_params.android_id
end
ngx.req.set_uri_args(args)
I try to do the same thing by golang, and my code is like this:
const newsTargetURL = "http://www.example.com/bd/news/home"
func GetNews(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "only get allowed", http.StatusMethodNotAllowed)
return
}
// deal params
rq := r.URL.Query()
os := rq.Get("os")
osv := rq.Get("osv")
imei := rq.Get("imei")
androidID := rq.Get("android_id")
deviceMake := rq.Get("make")
model := rq.Get("model")
deviceType := rq.Get("devicetype")
sw := rq.Get("w")
sh := rq.Get("h")
conn := rq.Get("conn")
carrier := rq.Get("carrier")
uv := make(url.Values)
uv.Set("media", "24")
uv.Set("submedia", "46")
uv.Set("os", os)
uv.Set("osv", osv)
if os == "1" {
uv.Set("imei", imei)
uv.Set("anid", androidID)
}
uv.Set("make", deviceMake)
uv.Set("model", model)
uv.Set("sw", sw)
uv.Set("sh", sh)
uv.Set("devicetype", deviceType)
uv.Set("ip", ip)
uv.Set("ua", ua)
uv.Set("conn", conn)
uv.Set("carrier", carrier)
t := newsTargetURL + "?" + uv.Encode()
// make a director
director := func(req *http.Request) {
u, err := url.Parse(t)
if err != nil {
panic(err)
}
req.URL = u
}
// make a proxy
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(w, r)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(GetNews))
srv := &http.Server{
Addr: ":2222",
Handler: mux,
}
srv.ListenAndServe()
}
I put this go version to the same server where lua version locate, but it does not work as lua file do. I read the httputil document but found nothing that can help. What do I need to do?

I wrote together a simple proxy for GET requests. Hope this helps.
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
const newsTargetURL = "http://www.example.com/bd/news/home"
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(GetNews))
srv := &http.Server{
Addr: ":2222",
Handler: mux,
}
// output error and quit if ListenAndServe fails
log.Fatal(srv.ListenAndServe())
}
func GetNews(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "only get allowed", http.StatusMethodNotAllowed)
return
}
// build proxy url
urlstr := fmt.Sprintf("%s?%s", newsTargetURL, r.URL.RawQuery)
// request the proxy url
resp, err := http.Get(urlstr)
if err != nil {
http.Error(w, fmt.Sprintf("error creating request to %s", urlstr), http.StatusInternalServerError)
return
}
// make sure body gets closed when this function exits
defer resp.Body.Close()
// read entire response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, "error reading response body", http.StatusInternalServerError)
return
}
// write status code and body from proxy request into the answer
w.WriteHeader(resp.StatusCode)
w.Write(body)
}
You can try it as is. It will work and show the content of example.com.
It uses a single handler GetNews for all requests. It skips all of the request parameter parsing and building by simply using r.url.RawQuery and newsTargetURL to build the new url.
Then we make a request to the new url (the main part missing in your question). From the response we read resp.StatusCode and resp.body to use in our response to the original request.
The rest is error handling.
The sample does not forward any additional information like cookies, headers, etc. That can be added as needed.

Related

How to rotate between multiple forwarding proxies for outgoing requests with golang

I will like to pass a list of forwarding proxy servers for POST request
Currently i am able to do it with just single forwarding proxy
serverProxy := "http://user:password#123.45.67.89:3128"
request, error := http.NewRequest("POST", httpposturl, bytes.NewBuffer(requestJSON))
request.Header.Set("Content-Type", "application/json; charset=UTF-8")
proxyURL, _ := url.Parse(serverProxy)
proxy := http.ProxyURL(proxyURL)
transport := &http.Transport{Proxy: proxy}
client := &http.Client{Transport: transport}
what i will like to do is pass a list to url.Parse and want it to use them using round robin balancing
so something like this
serverProxy := "http://user:password#123.45.67.89:3128, http://user:password#223.45.67.89:3128"
and then it will select which of the proxy servers to use and rotate them within requests
Is this possible?
UPDATE:
I want to be able to pass the rotated proxy server like this
proxyServer := roundRobin("http://round:robin#123.45.67.89:3128, http://robin:round#223.45.67.89:3128")
fmt.Println("proxy server used", proxyServer, "\n")
transport := &http.Transport{Proxy: proxyServer}
client := &http.Client{Transport: transport}
Create a proxy function that round-robins through your proxy URLs. Use that function in your transport:
func roundRobin(urls []*url.URL) func(*http.Request) (*url.URL, error) {
var mu sync.Mutex
var i int
return func(r *http.Request) (*url.URL, error) {
mu.Lock()
i = (i + 1) % len(urls)
u := urls[i]
mu.Unlock()
return u, nil
}
}
transport := &http.Transport{Proxy: roundRobin(yourProxyURLs)}
client := &http.Client{Transport: transport}
Here's the Montage's answer with explanation:
The requirement is to forward request through proxies in round-robin fashion
Since we are using http.Client to make request we can look at http.Client documentation to see if it provide any support for forwarding request through proxy when we look at the documentation we can see that it does support passing proxy which we can pass through http.Transport type which will be passed to http.Client. http.Transport takes proxy through Proxy field which takes in func that return *url.URL and error there are existing methods like http.ProxyURL and http.ProxyFromEnvironment provided within http package that we can use to pass proxies to http.Transport but the problem with these methods is that they only take a single proxy server which does not solve our problem at hand and hence we would require to create our own function which takes in multiple proxy servers urls and round-robin between them.
If we look at one of the existing method implemention as our base for creating our own method lets go with http.ProxyURL for our case the implementation can be found here. I have copied the implementation below
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error) {
return func(*Request) (*url.URL, error) {
return fixedURL, nil
}
}
we can see that its a simple closure which takes in single url and return a closure function which then intern return the url passed in as parameter. so we can take it base and create our own round-robin clouse function
func roundRobin(proxies ...string) func(*http.Request) (*url.URL, error) {
var urls []*url.URL
for _, proxy := range proxies {
u, err := url.Parse(proxy)
if err != nil {
log.Fatal(err)
}
urls = append(urls, u)
}
var mu sync.Mutex
var i, lenUrls int = 0, len(urls)
return func(r *http.Request) (*url.URL, error) {
mu.Lock()
i = (i + 1) % lenUrls
u := urls[i]
mu.Unlock()
return u, nil
}
}
Lets go over the roundRobin function implementation it is a variadic function which takes in proxy url(s) in string format as argument, which internally gets converted to url.URL by parsing the string using url.Parse then using the parsed url.URL to create slice of urls []*url.URL which then being used to forward request in round-robin fashion
Complete working example can be found below:
package main
import (
"fmt"
"log"
"net/url"
"net/http"
"sync"
)
func roundRobin(proxies ...string) func(*http.Request) (*url.URL, error) {
var urls []*url.URL
for _, proxy := range proxies {
u, err := url.Parse(proxy)
if err != nil {
log.Fatal(err)
}
urls = append(urls, u)
}
var mu sync.Mutex
var i, lenUrls int = 0, len(urls)
return func(r *http.Request) (*url.URL, error) {
mu.Lock()
i = (i + 1) % lenUrls
u := urls[i]
mu.Unlock()
return u, nil
}
}
func main() {
proxyFn := roundRobin("http://user:password#123.45.67.89:3128", "http://user:password#223.45.67.89:3128")
transport := &http.Transport{Proxy: proxyFn}
client := &http.Client{Transport: transport}
req, err := http.NewRequest("POST", "http://example.com", nil)
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
resp, err := client.Do(req)
if err != nil {
fmt.Println(resp)
}
fmt.Println(proxyFn(nil))
fmt.Println(proxyFn(nil))
}
Playground
Another version
package main
import (
"fmt"
"log"
"net/http"
"net/url"
"sync"
)
func praseUrls(proxies ...string) (urls []*url.URL) {
for _, proxy := range proxies {
u, err := url.Parse(proxy)
if err != nil {
log.Fatal(err)
}
urls = append(urls, u)
}
return
}
func roundRobin(max int) func() int {
var i int
return func() int {
i = (i + 1) % max
return i
}
}
func proxyFn(urls []*url.URL) func(*http.Request) (*url.URL, error) {
var m sync.Mutex
fn := roundRobin(len(urls))
return func(*http.Request) (*url.URL, error) {
m.Lock()
u := urls[fn()]
m.Unlock()
return u, nil
}
}
func main() {
proxies := []string{"http://user:password#123.45.67.89:3128", "http://user:password#223.45.67.89:3128"}
urls := praseUrls(proxies...)
transport := &http.Transport{Proxy: proxyFn(urls)}
client := &http.Client{Transport: transport}
req, err := http.NewRequest("POST", "http://example.com", nil)
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
resp, err := client.Do(req)
if err != nil {
fmt.Println(resp)
}
}
Playground
Note: It would be better to pass proxy urls from env variable which would help in case any proxy server changes or new are added
Here's the Montage's answer with code to parse a string.
func roundRobin(serverProxy string) func(*http.Request) (*url.URL, error) {
parts := strings.Split(serverProxy, ",")
var urls []*url.URL
for _, part := range parts {
u, err := url.Parse(strings.TrimSpace(part))
if err != nil {
log.Fatal(err)
}
urls = append(urls, u)
}
var mu sync.Mutex
var i int
return func(r *http.Request) (*url.URL, error) {
mu.Lock()
i = (i + 1) % len(urls)
u := urls[i]
mu.Unlock()
return u, nil
}
}
serverProxy := "http://user:password#123.45.67.89:3128, http://user:password#223.45.67.89:3128"
transport := &http.Transport{Proxy: roundRobin(serverProxy)}
client := &http.Client{Transport: transport}

Golang H2C Server doesn't write full body. Chrome says: "Server reset stream". Only occurs over http2 connection

Most Recent Version of Go (1.153)
Below is the code for reproducibility. Please try to access https://easy-dp.ngrok.io to see the issue.
Here's what I did:
Create a Reverse Proxy accessing Gzipped/ Br encoded Content
Request a publicly available URL, I just grabbed Google Analytics
Attempt to encode and decode the response via an http2 connection with a proxy.modifyresponse function
Watch as content is dropped.
However, this only occurs under the following conditions:
Under SSL, like with https://easy-dp.ngrok.io
When running a proxy.ModifyResponse function
Decompressing and re-compressing the body (for example, just reading and rewriting the body to new bytes works)
package main
import (
"bytes"
"compress/gzip"
"fmt"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"io/ioutil"
"net/http"
"net/http/httputil"
"strconv"
"time"
)
func ForwardAnalytics(req *http.Request) {
req.URL.Scheme = "https"
req.URL.Host = "www.google-analytics.com"
req.Host = "www.google-analytics.com"
req.URL.Path = "/analytics.js"
req.Header.Set("Accept-Encoding", "gzip")
}
func ModifyAnalytics(r *http.Response) error {
bytesFromBody, err := ioutil.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
return nil
}
if r.Header.Get("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(bytes.NewBuffer(bytesFromBody))
if err != nil {
return nil
}
defer gzipReader.Close()
readableBytes, err := ioutil.ReadAll(gzipReader)
var b bytes.Buffer
gzipWriter, err := gzip.NewWriterLevel(&b, gzip.DefaultCompression)
if err != nil {
return nil
}
defer gzipWriter.Close()
writtenLen, err := gzipWriter.Write(readableBytes)
fmt.Println("Wrote ", writtenLen)
if err != nil {
return nil
}
r.ContentLength = int64(len(readableBytes))
r.Header.Set("Content-Length", strconv.FormatInt(int64(len(readableBytes)), 10))
r.Body = ioutil.NopCloser(&b)
return nil
} else {
return nil
}
}
func handleProxy(w http.ResponseWriter, req *http.Request) {
proxy := httputil.ReverseProxy{
Director: ForwardAnalytics
}
proxy.ModifyResponse = ModifyAnalytics
proxy.ServeHTTP(w, req)
}
func main() {
h2s := &http2.Server{
IdleTimeout: 20 * time.Second,
}
mux := http.NewServeMux()
mux.HandleFunc( "/", handleProxy)
s := &http.Server{
ReadHeaderTimeout: 20 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
Addr: "localhost:8456",
Handler: h2c.NewHandler(mux, h2s),
}
s.ListenAndServe()
}
What did you expect to see?
I expect to see the ability to open the bytes, modify them, and update the response body on an H2C connection
What did you see instead?
Two things of note happen:
Chrome gives a nice little error that expands upon what's going on
{"params":{"description":"Server reset stream.","net_error":"ERR_HTTP2_PROTOCOL_ERROR","stream_id":5},"phase":0,"source":{"id":1493828,"start_time":"732370299","type":1},"time":"732375561","type":224},
Under the normal http connection, there's no problem, but under the https connection the script may or may not print out to a certain length. Sometimes it doesn't print at all, sometimes it prints about 30%.
This is a cross browser issue.
The Content-Length header indicates the size of the entity body in the message, in bytes. The size includes any content encodings (the Content-Length of a gzip-compressed text file will be the compressed size, not the original size).
src
I thought I had tried this but kept running into ERR_CONTENT_LENGTH_MISMATCH because of how I was closing my gzip writer. Related Question
Final handler looked like this:
if r.Header.Get("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(bytes.NewBuffer(bytesFromBody))
if err != nil {
return nil
}
defer gzipReader.Close()
readableBytes, err := ioutil.ReadAll(gzipReader)
var b bytes.Buffer
gzipWriter, err := gzip.NewWriterLevel(&b, gzip.DefaultCompression)
if err != nil {
return nil
}
writtenLen, err := gzipWriter.Write(readableBytes)
gzipWriter.Close() // This was the culprit. It needed to be closed here
fmt.Println("Wrote ", writtenLen)
if err != nil {
return nil
}
r.ContentLength = int64(b.Len())
r.Header.Set("Content-Length", strconv.FormatInt(int64(b.Len()), 10))
r.Body = ioutil.NopCloser(&b)
return nil
}

Proxy gateway send HTTP response back

I am looking to make a proxy gateway in Go.
Almost done ! One thing is still missing : send the entire client response to the server request.
I've got my own HTTP handler :
func (f HttpHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rurl, err := getOriginurl(r.RequestURI); err == nil {
[...]
client := &Http.Client{}
r.URL = rurl
r.RequestURI = ""
resp, err := client.Do(r)
if err == nil {
for k, vs := range resp.Header {
for _, v := range vs {
w.Header().Set(k, v)
}
}
w.WriteHeader(resp.StatusCode)
if responseData,err := ioutil.ReadAll(resp.Body); err == nil {
w.Write(responseData)
}
}
}
}
func getOriginurl(request string) *url.URL {
{...}
// Would return an *url.URL with : http://127.0.0.1:8080/{requestURI}
}
I am looking for a way to optimize the way to parse Client response to ResponseWriter.
Actually my question would be : How to parse Response type to ResponseWriter exhaustively ?
You can use httputil.NewSingleHostReverseProxy instead of your own HTTP client logic.
httputil.NewSingleHostReverseProxy(rurl).ServeHTTP(w, r)

Too many open files serving http

I have the following code
package main
import (
"bytes"
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
"time"
"io"
httprouter "github.com/fasthttp/router"
"github.com/valyala/fasthttp"
)
func main() {
router := mux.NewRouter().StrictSlash(true)
/*router := NewRouter()*/
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello!!!")
})
router.HandleFunc("/{name}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
prepare(w, r, vars["name"])
}).Methods("POST")
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", 8080), router))
}
//using fast http
func _() {
router := httprouter.New()
router.GET("/", func(w *fasthttp.RequestCtx) {
_, _ = fmt.Fprintf(w, "Hello!!!")
})
router.POST("/:name", func(w *fasthttp.RequestCtx) {
prepareRequest(w, w.UserValue("name").(string))
})
log.Fatal(fasthttp.ListenAndServe(fmt.Sprintf(":%d", 8080), router.Handler))
}
//func prepare(w *fasthttp.RequestCtx, name string)
func prepare(w http.ResponseWriter, r *http.Request, name string) {
//other part of the code and call to goroutine
var urls []string
//lets say all the url loaded, call the go routine func and wait for channel to respond and then proceed with the response of all url
results := callUrls(urls) //there are 10 urls atleast to call simultaneously for each request everytime
process(w, results)
}
type Response struct {
status int
url string
body string
}
func callUrls(urls []string) []*Response {
ch := make(chan *Response, len(urls))
for _, url := range urls {
go func(url string) {
//http post on url,
//base on status code of url call, add to status code
//some thing like
req, err := http.NewRequest("POST", url, bytes.NewBuffer(somePostData))
req.Header.Set("Content-Type", "application/json")
req.Close = true
client := &http.Client{
Timeout: time.Duration(time.Duration(100) * time.Millisecond),
}
response, err := client.Do(req)
//Using fast http client
/*req := fasthttp.AcquireRequest()
req.SetRequestURI(url)
req.Header.Set("Content-Type", "application/json")
req.Header.SetMethod("POST")
req.SetBody(somePostData)
response := fasthttp.AcquireResponse()
client := &fasthttp.Client{
ReadTimeout: time.Duration(time.Duration(100) * time.Millisecond),
}
err := client.Do(req, response)*/
if err != nil {
//do other thing with the response received
_, _ = io.Copy(ioutil.Discard, response.Body)
_ = response.Body.Close()
} else {
//success response
_, _ = io.Copy(ioutil.Discard, response.Body)
_ = response.Body.Close()
body, _:= ioutil.ReadAll(response.Body)
strBody := string(body)
strBody = strings.Replace(strBody, "\r", "", -1)
strBody = strings.Replace(strBody, "\n", "", -1)
}
// return to channel accordingly
ch <- &Response{200, "url", "response body"}
}(url)
}
var results []*Response
for {
select {
case r := <-ch:
results = append(results, r)
if len(results) == len(urls) {
//Done
close(ch)
return results
}
}
}
}
//func process(w *fasthttp.RequestCtx,results []*Response){
func process(w http.ResponseWriter, results []*Response){
fmt.Println("response", "response body")
}
After serving few request on multi core CPU (there are around 4000-6000 req coming per sec) I get too many files open error and response time and CPU goes beyond limit. (Could CPU be be high because I convert byte to string a few times to replace few character? Any suggestion?)
I have seen other question referring to closing req/res body and/or setting sysctl or ulimit to higher values, I did follow those but I always end up with the error.
Config on the server:
/etc/sysctl.conf net.ipv4.tcp_tw_recycle = 1
open files (-n) 65535
I need the code to respond in millisec but it take upto 50sec when cpu is high.
Have tried both net/http and fast http but with no improvement. My Node.js request npm does everything perfectly on the same server. What will be best way to handle those connection or change in the code needed for improvement.
You can use the following library:
Requests: A Go library for reduce the headache when making HTTP requests (20k/s req)
https://github.com/alessiosavi/Requests
It's developed for solve theto many open files dealing with parallel requests.
The idea is to allocate a list of request, than send them with a configurable "parallel" factor that allow to run only "N" request at time.
Initialize the requests (you have already a set of urls)
// This array will contains the list of request
var reqs []requests.Request
// N is the number of request to run in parallel, in order to avoid "TO MANY OPEN FILES. N have to be lower than ulimit threshold"
var N int = 12
// Create the list of request
for i := 0; i < 1000; i++ {
// In this case, we init 1000 request with same URL,METHOD,BODY,HEADERS
req, err := requests.InitRequest("https://127.0.0.1:5000", "GET", nil, nil, true)
if err != nil {
// Request is not compliant, and will not be add to the list
log.Println("Skipping request [", i, "]. Error: ", err)
} else {
// If no error occurs, we can append the request created to the list of request that we need to send
reqs = append(reqs, *req)
}
}
At this point, we have a list that contains the requests that have to be sent.
Let's send them in parallel!
// This array will contains the response from the givens request
var response []datastructure.Response
// send the request using N request to send in parallel
response = requests.ParallelRequest(reqs, N)
// Print the response
for i := range response {
// Dump is a method that print every information related to the response
log.Println("Request [", i, "] -> ", response[i].Dump())
// Or use the data present in the response
log.Println("Headers: ", response[i].Headers)
log.Println("Status code: ", response[i].StatusCode)
log.Println("Time elapsed: ", response[i].Time)
log.Println("Error: ", response[i].Error)
log.Println("Body: ", string(response[i].Body))
}
You can find example usage into the example folder of the repository.
SPOILER:
I'm the author of this little library

Go http, send incoming http.request to an other server using client.Do

Here my use case
We have one services "foobar" which has two version legacy and version_2_of_doom (both in go)
In order to make the transition from legacy to version_2_of_doom , we would like in a first time, to have the two version alongside, and have the POST request (as there's only one POST api call in this ) received on both.
The way I see how to do it. Would be
modifying the code of legacy at the beginning of the handler, in order to duplicate the request to version_2_of_doom
func(w http.ResponseWriter, req *http.Request) {
req.URL.Host = "v2ofdoom.local:8081"
req.Host = "v2ofdoom.local:8081"
client := &http.Client{}
client.Do(req)
// legacy code
but it seems to not be as straightforward as this
it fails with http: Request.RequestURI can't be set in client requests.
Is there a well-known method to do this kind of action (i.e transfering without touching) a http.Request to an other server ?
You need to copy the values you want into a new request. Since this is very similar to what a reverse proxy does, you may want to look at what "net/http/httputil" does for ReverseProxy.
Create a new request, and copy only the parts of the request you want to send to the next server. You will also need to read and buffer the request body if you intend to use it both places:
func handler(w http.ResponseWriter, req *http.Request) {
// we need to buffer the body if we want to read it here and send it
// in the request.
body, err := ioutil.ReadAll(req.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// you can reassign the body if you need to parse it as multipart
req.Body = ioutil.NopCloser(bytes.NewReader(body))
// create a new url from the raw RequestURI sent by the client
url := fmt.Sprintf("%s://%s%s", proxyScheme, proxyHost, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, url, bytes.NewReader(body))
// We may want to filter some headers, otherwise we could just use a shallow copy
// proxyReq.Header = req.Header
proxyReq.Header = make(http.Header)
for h, val := range req.Header {
proxyReq.Header[h] = val
}
resp, err := httpClient.Do(proxyReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// legacy code
}
In my experience, the easiest way to achieve this was to simply create a new request and copy all request attributes that you need into the new request object:
func(rw http.ResponseWriter, req *http.Request) {
url := req.URL
url.Host = "v2ofdoom.local:8081"
proxyReq, err := http.NewRequest(req.Method, url.String(), req.Body)
if err != nil {
// handle error
}
proxyReq.Header.Set("Host", req.Host)
proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
client := &http.Client{}
proxyRes, err := client.Do(proxyReq)
// and so on...
This approach has the benefit of not modifying the original request object (maybe your handler function or any middleware functions that are living in your stack still need the original object?).
Using original request (copy or duplicate only if original request still need):
func handler(w http.ResponseWriter, r *http.Request) {
// Step 1: rewrite URL
URL, _ := url.Parse("https://full_generic_url:123/x/y")
r.URL.Scheme = URL.Scheme
r.URL.Host = URL.Host
r.URL.Path = singleJoiningSlash(URL.Path, r.URL.Path)
r.RequestURI = ""
// Step 2: adjust Header
r.Header.Set("X-Forwarded-For", r.RemoteAddr)
// note: client should be created outside the current handler()
client := &http.Client{}
// Step 3: execute request
resp, err := client.Do(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Step 4: copy payload to response writer
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
resp.Body.Close()
}
// copyHeader and singleJoiningSlash are copy from "/net/http/httputil/reverseproxy.go"
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
I've seen the accepted anwser, but I would like to say that I dont like this. I've used this code for months with it working, but after some time you encounter requests that break (POST requests in my case). My preferred solution is the following:
r.URL.Host = "example.com"
r.RequestURI = ""
client := &http.Client{}
delete(r.Header, "Accept-Encoding")
delete(r.Headers, "Content-Length")
resp, err := client.Do(r.WithContext(context.Background())
if err != nil {
return nil, err
}
return resp, nil

Resources