Proper way of extracting tracer from request Headers for opentelemetry - go

I have krakend apigateway which is using opentracing and sending req headers as X-B3-... for tracing and my service is using opentelemetry.
This is what I'm having right now on jaeger.
enter image description here
enter image description here
I want the service spans to come under the apigateways'.
Workaround I have done:
This is the exact handler for the request using chi router.
func (env *Env) getCommentForBlogRouter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
keys := []string{"X-B3-Traceid", "X-B3-Spanid", "X-B3-Sampled"}
var carrier propagation.HeaderCarrier = propagation.HeaderCarrier{}
for _, k := range keys {
carrier.Set(strings.ToLower(k), r.Header[k][0])
}
var propagator propagation.TextMapPropagator = otel.GetTextMapPropagator()
ctx = propagator.Extract(ctx, carrier)
// fmt.Println(ctx)
tr := otel.Tracer("Handler: blog-comments")
ctx, span := tr.Start(ctx, "handler span")
defer span.End()
blogId := r.URL.Query().Get("blog-id")
span.SetAttributes(attribute.Key("blog-id").String(fmt.Sprint(blogId)))
var spanDB trace.Span
ctx, spanDB = tr.Start(ctx, "Select row")
comments, err := env.comments.GetForBlog(blogId)
spanDB.End()
var spanRes trace.Span
_, spanRes = tr.Start(ctx, "Sending Response")
defer spanRes.End()
if err != nil {
fmt.Println(err)
SendError(w, http.StatusInternalServerError, "Something went wrong")
return
}
if comments == nil {
comments = []models.Comment{}
}
SendResponse(w, http.StatusOK, map[string]interface{}{
"data": comments,
})
}

Ok, I figured out how to make it work.
I added this middleware and it will sync the request context with the headers traceID, spanID and traceFlags.
After this we are good to create tracer and spans as we want.
func Tracing(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceID, _ := trace.TraceIDFromHex(r.Header["X-B3-Traceid"][0])
spanID, _ := trace.SpanIDFromHex(r.Header["X-B3-Spanid"][0])
var traceFlags trace.TraceFlags
if r.Header["X-B3-Sampled"][0] == "1" {
traceFlags = trace.FlagsSampled
}
spanContext := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: traceFlags,
})
ctx := trace.ContextWithSpanContext(r.Context(), spanContext)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

Related

How to pass request scope variables to promethues handler function

i am building an promethues exporter in golang, the url to the exporter will be http://exporter-ip:9000/unique-id/metrics.
By parsing the url in ProcessParameters() function i am getting unique-id and with unique-id i am getting ip,username,password.
how can i pass IP, Username, Password from ProcessParameters() middleware function to Collect() function.
There variables are request scoped
func (collector *Collector) Collect(ch chan<- prometheus.Metric) {
//need IP,Username & Password here.
}
func ProcessParameters(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Print("Executing middlewareOne")
DeviceID = strings.Split(r.URL.Path, "/")[1] //getting unique ID from the URL and verifying if that id is valid
_, ok := util.Devices[DeviceID] //if device id is not present in map, return StatusForbidden error in if block.
if !ok{
errMsg := "Device not found"
http.Error(w, errMsg, http.StatusForbidden)
log.Println(errMsg)
w.WriteHeader(http.StatusForbidden)
w.Header().Set("Content-Type", "application/json")
resp := make(map[string]string)
resp["message"] = "Forbidden"
jsonResp, err := json.Marshal(resp)
if err != nil {
log.Fatalf("Error happened in JSON marshal. Err: %s", err)
}
w.Write(jsonResp)
} else { //if id is present pass controller final handler(deviceHandler)
// tried setting it to request context also how to access it from collect() func
ctx := context.WithValue(r.Context(), "IP", util.Devices["10.0.0.1"])
context.WithValue(r.Context(), "UserName", util.Devices["user1"])
context.WithValue(r.Context(), "Password", util.Devices["pass1"])
next.ServeHTTP(w, r.WithContext(ctx))
}
})
}
func main() {
collector := metricsCollector()
registry := prometheus.NewRegistry()
registry.Register(collector)
deviceHandler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
mux := http.NewServeMux()
mux.Handle("/", ProcessParameters(deviceHandler)) // how can pass variables from ProcessParameters() middleware handler to deviceHandler
err := http.ListenAndServe(":9090", mux)
log.Fatal(err)
}

Golang Fiber and Auth0

I'm new to golang, and have followed this (https://auth0.com/blog/authentication-in-golang/) auth0 guide, for setting up a go rest api.
I'm struggeling with converting to Fiber, and in the same time putting my functions that are being called by routes, out to seperate files.
Currently my main file looks like this:
func main() {
r := mux.NewRouter()
r.Handle("/", http.FileServer(http.Dir("./views/")))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
r.Handle("/posts", config.JwtMiddleware.Handler(GetPosts)).Methods("GET")
//r.Handle("/products/{slug}/feedback", jwtMiddleware.Handler(AddFeedbackHandler)).Methods("POST")
// For dev only - Set up CORS so React client can consume our API
corsWrapper := cors.New(cors.Options{
AllowedMethods: []string{"GET", "POST"},
AllowedHeaders: []string{"Content-Type", "Origin", "Accept", "*"},
})
http.ListenAndServe(":8080", corsWrapper.Handler(r))
}
var GetPosts= http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
collection, err := config.GetMongoDbCollection(dbName, collectionName)
if err != nil {
fmt.Println("Error")
}else{
fmt.Println(collection)
//findOptions := options.Find()
cursor, err := collection.Find(context.Background(), bson.M{})
if err != nil {
log.Fatal(err)
}
var posts[]bson.M
if err = cursor.All(context.Background(), &posts); err != nil {
log.Fatal(err)
}
fmt.Println(posts)
payload, _ := json.Marshal(posts)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(payload))
}
})
So I would like to convert from: r := mux.NewRouter() to fiber and in the same time move my GetPosts function out in a seperate file. When doing this, I can't seem to continue calling my jwtMiddleware.
I have tried this package: https://github.com/Mechse/fiberauth0 but it seems like its broken. At least I can call protected routes without supplying jwt tokens in my header.
You can simply convert 'net/http' style middleware handlers with the provided adaptor package(https://github.com/gofiber/adaptor). Note you need to make some changes to the function signature provided by auth0 but this works;
// EnsureValidToken is a middleware that will check the validity of our JWT.
func ensureValidToken(next http.Handler) http.Handler {
issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
if err != nil {
log.Fatalf("Failed to parse the issuer url: %v", err)
}
provider := jwks.NewCachingProvider(issuerURL, 5*time.Minute)
jwtValidator, err := validator.New(
provider.KeyFunc,
validator.RS256,
issuerURL.String(),
[]string{os.Getenv("AUTH0_AUDIENCE")},
validator.WithCustomClaims(
func() validator.CustomClaims {
return &CustomClaims{}
},
),
validator.WithAllowedClockSkew(time.Minute),
)
if err != nil {
log.Fatalf("Failed to set up the jwt validator")
}
errorHandler := func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Encountered error while validating JWT: %v", err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"message":"Failed to validate JWT."}`))
}
middleware := jwtmiddleware.New(
jwtValidator.ValidateToken,
jwtmiddleware.WithErrorHandler(errorHandler),
)
return middleware.CheckJWT(next)
}
var EnsureValidToken = adaptor.HTTPMiddleware(ensureValidToken)
app := fiber.New()
app.Use(EnsureValidToken)
app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
app.Listen(":3000")

How to clear the session and only visit an about page after login?

This code has two parts. One is to set and clear the session and the second part is login and logout.
What it does?
In the second part, If an email and password are found in the database and the match is true then it set the session and move to the about() function which has an about file. If the logout is called then it clears the session and redirects to the home page.
What it should do?
The problem is that even if I am logged out and the session is cleared, I can still visit an about page. I don't want to be allowed to visit an about page if I am not logged in.
First part
var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
)
func setSession(email, password string, res http.ResponseWriter) {
value := map[string]string{ "email": email, "password": password}
encoded, err := cookieHandler.Encode("session", value)
if err == nil {
cookie := &http.Cookie{ Name: "session", Value: encoded, Path: "/"}
http.SetCookie(res, cookie)
}
}
func clearSession(res http.ResponseWriter) {
cookie := &http.Cookie{ Name: "session", Value: "", Path: "/", MaxAge: -1}
http.SetCookie(res, cookie)
}
Second part
func about(res http.ResponseWriter, req *http.Request) {
if err := tmpl.ExecuteTemplate(res, "about.html", nil); err != nil {
log.Fatal("template didn't execute", nil)
}
}
func loginAuth(res http.ResponseWriter, req *http.Request) {
email := req.FormValue("email")
password := req.FormValue("password")
match := database.Findaccount(email, password)
if match == true {
setSession(email, password, res)
about(res, req)
fmt.Println("You're logged in")
} else {
tmpl.ExecuteTemplate(res, "login.html", nil)
fmt.Println("Enter the correct email or password")
}
}
func logout(res http.ResponseWriter, req *http.Request) {
clearSession(res)
http.Redirect(res, req, "/", 302)
}
Few things you don't want to do, in general:
Don't use cookie encoder directly. Use a cookie session store.
Don't call an handler within an handler, prefer a redirect. This should prevent writing twice the headers/body on the response.
Don't pass the user/password in the cookie, even encoded, in 2021 we may even prevent sending that through the form at all (you might consider sending only a hash and re hash the hash on the server side to figure out if things are good to go).
Few things you always want to do:
Write tests.
Make use of middlewares.
Always provide small reproducible examples.
That being said, I have written a lookalike code with some stubs (mostly for db), I removed template support (i was not in the mood to write HTML) and more importantly I wrote tests !!
To the question How to clear the session :
Delete the values from the store, write the store
To the question and only visit an about page after login?:
Wrap that handler with a middleware that verifies login data attached to the user cookie store
-- main.go --
package main
import (
"crypto/sha256"
"encoding/gob"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
)
// Note: Don't store your key in your source code. Pass it via an
// environmental variable, or flag (or both), and don't accidentally commit it
// alongside your code. Ensure your key is sufficiently random - i.e. use Go's
// crypto/rand or securecookie.GenerateRandomKey(32) and persist the result.
var store = sessions.NewCookieStore(
securecookie.GenerateRandomKey(32),
)
//emulate db package
func dbLookupUser(user, pwd string) bool {
return user == "user" && pwd == "pwd"
}
func dbLookupHash(h string) bool {
return h == hash("user", "pwd")
}
func hash(s ...interface{}) string {
hr := sha256.New()
fmt.Fprint(hr, s...)
return fmt.Sprintf("%x", hr.Sum(nil))
}
// hashKey is a typed key for the session map store to prevent unintented overwrites.
type hashKey string
func init() {
gob.Register(hashKey(""))
}
func loginAuth(res http.ResponseWriter, req *http.Request) {
email := req.FormValue("email")
password := req.FormValue("password")
match := dbLookupUser(email, password)
if match == true {
session, _ := store.Get(req, "session-name")
session.Values["hash"] = hash(email, password)
// Save it before we write to the response/return from the handler.
err := session.Save(req, res)
if err == nil {
// about(res, req) // don't!
// the about handler might want to setup its own http response headers
// That would conflict with what we did here.
// prefer a redirect
http.Redirect(res, req, "/about", http.StatusFound)
return
}
} else {
fmt.Fprintf(res, "try again") // use a templatee instead!
// tmpl.ExecuteTemplate(res, "login.html", nil)
}
}
func logout(res http.ResponseWriter, req *http.Request) {
session, _ := store.Get(req, "session-name")
delete(session.Values, hashKey("hash"))
_ = session.Save(req, res)
http.Redirect(res, req, "/", 302)
}
func about(res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "welcome to about page")
}
func requireLogin(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "session-name")
var h string
if x, ok := session.Values[hashKey("hash")]; ok {
h = x.(string)
}
var match bool
if h != "" {
match = dbLookupHash(h)
}
if !match {
// Write an error and stop the handler chain
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next(w, r)
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/", loginAuth)
r.HandleFunc("/logout", logout)
r.HandleFunc("/about", requireLogin(about))
log.Fatal(http.ListenAndServe("localhost:8080", r))
}
-- main_test.go --
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func TestLogin(t *testing.T) {
req := httptest.NewRequest("POST", "http://example.com/foo", nil)
form := url.Values{}
form.Set("email", "user")
form.Set("password", "pwd")
req.Form = form
w := httptest.NewRecorder()
loginAuth(w, req)
resp := w.Result()
// body, _ := io.ReadAll(resp.Body)
if wanted := http.StatusFound; resp.StatusCode != wanted {
t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
}
// implement more check
}
func TestLoginFailure(t *testing.T) {
req := httptest.NewRequest("POST", "http://example.com/foo", nil)
form := url.Values{}
form.Set("email", "!user")
form.Set("password", "!pwd")
req.Form = form
w := httptest.NewRecorder()
loginAuth(w, req)
resp := w.Result()
// body, _ := io.ReadAll(resp.Body)
if wanted := http.StatusOK; resp.StatusCode != wanted {
t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
}
// implement more check
}
func TestAboutNotLogged(t *testing.T) {
req := httptest.NewRequest("POST", "http://example.com/foo", nil)
w := httptest.NewRecorder()
requireLogin(about)(w, req)
resp := w.Result()
// body, _ := io.ReadAll(resp.Body)
if wanted := http.StatusForbidden; resp.StatusCode != wanted {
t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
}
// implement more check
}
func TestAboutLogged(t *testing.T) {
req := httptest.NewRequest("POST", "http://example.com/foo", nil)
w := httptest.NewRecorder()
session, _ := store.Get(req, "session-name")
session.Values[hashKey("hash")] = hash("user", "pwd")
err := session.Save(req, w)
if err != nil {
t.Fatal(err)
}
hdr := w.Header()
req.Header.Add("Cookie", hdr["Set-Cookie"][0])
w = httptest.NewRecorder()
requireLogin(about)(w, req)
resp := w.Result()
// body, _ := io.ReadAll(resp.Body)
if wanted := http.StatusOK; resp.StatusCode != wanted {
t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
}
// implement more check
}

how to make a proxy by golang

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.

Golang: Can I remove response headers coming from ReverseProxy?

I'm using httputil.ReverseProxy to proxy Amazon s3 files to my clients. I'd like to hide all headers coming from Amazon - is that possible without having to reimplement Reverse Proxy?
proxy := httputil.ReverseProxy{Director: func(r *http.Request) {
r.Header = http.Header{} // Don't send client's request headers to Amazon.
r.URL = proxyURL
r.Host = proxyURL.Host
}}
proxy.ServeHTTP(w, r) // How do I remove w.Headers ?
You can implement ReverseProxy.Transport
type MyTransport struct{
header http.Header
}
func (t MyTransport) RoundTrip(r *Request) (*Response, error){
resp, err := http.DefaultTransport.RoundTrip(r)
resp.Header = t.header
return resp, err
}
mytransport := MyTransport{
//construct Header
}
proxy := httputil.ReverseProxy{Director: func(r *http.Request) {
r.Header = http.Header{} // Don't send client's request headers to Amazon.
r.URL = proxyURL
r.Host = proxyURL.Host
},
Transport: mytransport,
}
This was my solution to remove/replace all the http.ReverseProxy response headers:
type responseHeadersTransport http.Header
func (t responseHeadersTransport) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
return nil, err
}
resp.Header = http.Header(t)
return resp, nil
}
func ProxyFile(w http.ResponseWriter, r *http.Request) {
// ...
headers := http.Header{}
headers.Set("Content-Type", file.ContentType)
headers.Set("Content-Length", fmt.Sprintf("%d", file.Filesize))
headers.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", file.Filename))
proxy := httputil.ReverseProxy{
Director: func(r *http.Request) { // Remove request headers.
r.Header = http.Header{}
r.URL = proxyURL
r.Host = proxyURL.Host
},
Transport: responseHeadersTransport(headers), // Replace response headers.
}
proxy.ServeHTTP(w, r)
}

Resources