gin icon indicating copy to clipboard operation
gin copied to clipboard

base64 Unmarshalling from c.ShouldBindQuery not working

Open vkstack opened this issue 1 year ago • 1 comments

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 avatar Aug 05 '22 22:08 vkstack

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()
	}
}
  1. 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?

vkstack avatar Aug 06 '22 16:08 vkstack