gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
78.09k stars 7.97k forks source link

base64 Unmarshalling from c.ShouldBindQuery not working #3269

Open vkstack opened 2 years ago

vkstack commented 2 years ago

Description

I have a base64 yJUaW1lIjoiMjAyMi0wOC0wNlQxODoxMzo1Ni45OTA3MjkrMDU6MzAifQo= which decodes to {"Time":"2022-08-06T18:13:56.990729+05:30"}

Now I wanted to pass this encoded base64 in queryparam and unmarshal it to struct. But I found it was not happening. every time i tried, I got.

{
    "detail": {
      "Offset": 1
    },
    "error": "invalid character 'e' looking for beginning of value"
}

Is there not any way to unmarshal it in struct while passing base64 encoded data in queryparams?

How to reproduce

*Updated Aug 6,2022 6:25 PM

package main

import (
    "encoding/base64"
    "encoding/json"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.Any("/health", health)
    router.Run(":8080")
}

type Temp struct {
    Time time.Time
}

type Request struct {
    Token Temp `json:"token" form:"token"`
}

func (t *Temp) UnmarshalJSON(src []byte) error {
    b64len := len(src) - 2
    src = src[1 : b64len+1]
    dst := make([]byte, base64.StdEncoding.DecodedLen(len(src)))
    n, err := base64.StdEncoding.Decode(dst, src)
    if err != nil {
        return err
    }
    type __ Temp
    var x __
    if err := json.Unmarshal(dst[:n], &x); err != nil {
        return err
    }
    *t = Temp(x)
    return nil
}

func health(c *gin.Context) {
    var req Request
    var err error
    if c.Request.Method == "GET" {
        err = c.ShouldBindQuery(&req)
    } else if c.Request.Method == "POST" {
        err = c.ShouldBindJSON(&req)
    } else {
        return
    }
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error":  err.Error(),
            "detail": err,
        })
        return
    }
    c.JSON(http.StatusOK, req)
}

Expectations

$ curl http://localhost:8080/health?token=eyJ0b2tlbiI6eyJUaW1lIjoiMjAyMi0wOC0wNlQxODoxMToxNi4xMjA3NCswNTozMCJ9fQo=

{
    "token": {
        "Time": "2022-08-06T18:13:56.990729+05:30"
    }
}

Actual result

$ curl http://localhost:8080/health?token=eyJ0b2tlbiI6IjIwMjItMDgtMDZUMDM6MjI6MjcuNzM1ODE0KzA1OjMwIn0K
{
    "detail": {
      "Offset": 1
    },
    "error": "invalid character 'e' looking for beginning of value"
}

Note

With Post I am getting expected response.

curl --location --request POST 'http://localhost:8080/health' \
--header 'Content-Transfer-Encoding: base64' \
--header 'Content-Type: application/json' \
--data-raw '{
    "token": "eyJUaW1lIjoiMjAyMi0wOC0wNlQxODoxMzo1Ni45OTA3MjkrMDU6MzAifQo="
}'

{
    "token": {
        "Time": "2022-08-06T18:13:56.990729+05:30"
    }
}

Environment

go version go1.19 darwin/amd64

go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/vajahat/Library/Caches/go-build"
GOENV="/Users/vajahat/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/vajahat/go/pkg/mod"
GONOPROXY="gitlab.com/dotcomino"
GONOSUMDB="gitlab.com/dotcomino"
GOOS="darwin"
GOPATH="/Users/vajahat/go"
GOPRIVATE="gitlab.com/dotcomino"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.19"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/lf/j1n10m694bv_xvzgxw9_nwh40000gn/T/go-build14186801=/tmp/go-build -gno-record-gcc-switches -fno-common"
vkstack commented 2 years ago

There are 2 approaches I have discovered so far.

  1. Either I write a middleware
    func tokenprocessor(c *gin.Context) {
    if x, ok := c.GetQuery("token"); ok {
        if x[len(x)-1] != '"' {
            x = x + "\""
        }
        if x[0] != '"' {
            x = "\"" + x
        }
        fmt.Println(x)
        val := c.Request.URL.Query()
        val.Set("token", x)
        c.Request.URL.RawQuery = val.Encode()
    }
    }
  2. Or I write a custom binder.
    
    const defaultMemory = 32 << 20

type SpecialTokenBinder struct{}

var SpltokenBinder = SpecialTokenBinder{}

func (SpecialTokenBinder) Name() string { return "form" }

func (SpecialTokenBinder) Bind(req *http.Request, obj interface{}) error { if err := req.ParseForm(); err != nil { return err } if err := req.ParseMultipartForm(defaultMemory); err != nil { if err != http.ErrNotMultipart { return err } } if x := req.URL.Query().Get("token"); x != "" { if x[len(x)-1] != '"' { x = x + "\"" } if x[0] != '"' { x = "\"" + x } val := req.URL.Query() val.Set("token", x) req.URL.RawQuery = val.Encode() }

if err := binding.MapFormWithTag(obj, req.URL.Query(), "form"); err != nil {
    return err
}
if binding.Validator != nil {
    return binding.Validator.ValidateStruct(obj)
}
return nil

}



# Couldn't both of these have avoided?