I'm trying this neat framework (gorm) but I'm facing some issues with a basic read query.
What I`m doing is basically what the documentation says about how to read from a SQL Server database.
I got a database with a table tests (plural from the struct name Test). There is a single entry in the database (check the image), and I cannot retrieve it...
var server = "localhost"
var port = 1433
var user = "orm"
var password = "123localtest"
var database = "go"
var db *sql.DB
type Test struct {
ID int
Name string
}
func main() {
connectionString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=%s",
server, user, password, port, database)
db, err := gorm.Open("mssql", connectionString)
if err != nil {
log.Fatal("Failed to create connection pool. Error: " + err.Error())
}
gorm.DefaultCallback.Create().Remove("mssql:set_identity_insert")
defer db.Close()
tableExists := db.HasTable(&Test{})
if tableExists {
ReadFirstTest(db)
}
}
// ReadFirstTest Gets and prints first Test from dbo.test
func ReadFirstTest(db *gorm.DB) {
var test = Test{}
db.First(&test)
fmt.Printf("%d %s \n", test.ID, test.Name)
db.Where("name = ?", "John").First(&test)
fmt.Printf("%d %s \n", test.ID, test.Name)
}
Can you tell me what is missing here ?
Database state
I have been pulling my hair out recently, I need to find a way to save cookies after a post request to a URI, so that I can send requests to other endpoints and maintain that session. I am trying to add an item to cart but without saving the cookies the cart will be empty. (shopping cart) I am currently using this to handle cookies but doesn't seem to forward the cookies to next request:
func (c *CookieClient) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
for {
zap.S().Info("Saving Cookie")
if err := c.Client.Do(req, resp); err != nil {
return err
}
statusCode := resp.Header.StatusCode()
if statusCode != fasthttp.StatusMovedPermanently &&
statusCode != fasthttp.StatusFound &&
statusCode != fasthttp.StatusSeeOther &&
statusCode != fasthttp.StatusTemporaryRedirect &&
statusCode != fasthttp.StatusPermanentRedirect {
break
}
location := resp.Header.PeekBytes(strLocation)
if len(location) == 0 {
return fmt.Errorf("Redirect with missing Location header")
}
u := req.URI()
u.UpdateBytes(location)
resp.Header.VisitAllCookie(func(key, value []byte) {
c := fasthttp.AcquireCookie()
defer fasthttp.ReleaseCookie(c)
c.ParseBytes(value)
if expire := c.Expire(); expire != fasthttp.CookieExpireUnlimited && expire.Before(time.Now()) {
zap.S().Info("Deleting Expired Cookie")
req.Header.DelCookieBytes(key)
} else {
req.Header.SetCookieBytesKV(key, c.Value())
}
})
}
return nil
}
Probably the authors can have an efficient way:
You can retrieve the cookie with the following method, then you can reassign it to another request.
func ParseTokenFromRequest(ctx *fasthttp.RequestCtx) string {
token := string(ctx.Request.Header.Cookie("GoLog-Token")) // GoLog-Token is the hardcoded name of the cookie
return token
}
Then you can create the cookie with the value already retrieved:
//CreateCookie Method that return a cookie valorized as input (GoLog-Token as key)
func CreateCookie(key string, value string, expire int) *fasthttp.Cookie {
if strings.Compare(key, "") == 0 {
key = "GoLog-Token"
}
log.Debug("CreateCookie | Creating Cookie | Key: ", key, " | Val: ", value)
authCookie := fasthttp.Cookie{}
authCookie.SetKey(key)
authCookie.SetValue(value)
authCookie.SetMaxAge(expire)
authCookie.SetHTTPOnly(true)
authCookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
return &authCookie
}
And then you can forward the cookie or save it into a (maybe in-memory) db:
authcookie := CreateCookie("GoLog-Token", token, cfg.Redis.Token.Expire)
ctx.Response.Header.SetCookie(authcookie)
// store cookie here
I used go-swagger and gorm for MySQL queries and one of my handlers is (retreiving one record)
api.UsersUserGetByIDHandler = users.UserGetByIDHandlerFunc(func(params users.UserGetByIDParams) middleware.Responder {
db := dbConn()
user := User{}
res := db.Table("users").Where("id = ?", params.UserID).Select("id, email, password, name").Scan(&user)
if res.RecordNotFound() {
message := "User not exists"
return users.NewUserGetByIDDefault(500).WithPayload(&models.Error{Message: &message})
}
log.Info(user) // {21 bxffcgb#emagggil.com 123456 Second}
return users.NewUserGetByIDOK() //How return right response there???
//.WriteResponse()
})
or retreive all data from table users
api.UsersUserListHandler = users.UserListHandlerFunc(func(params users.UserListParams) middleware.Responder {
db := dbConn()
var user []User
var count int
db.Table("users").Select("id, email, password, name").Scan(&user).Count(&count)
log.Info(db.RecordNotFound())
if count == 0 {
message := "User not exists"
return users.NewUserGetByIDDefault(500).WithPayload(&models.Error{Message: &message})
}
return users.NewUserGetByIDOK()
})
User Gorm struct is
type User struct { // user
ID int64 `gorm:"AUTO_INCREMENT"`
Email string `gorm:"type:varchar(200);unique_index"`
Password string `gorm:"size:200"`
Name string `gorm:"type:varchar(200)`
}
and same as models.Users
How properly return data there? I tried with WriteResponse and WithPayload but unsuccessful
There is an answer:
First change
user := User{}
to
user := new(models.Users)
and add at the end
ret := make([]*models.Users, 0)
ret = append(ret, user)
return users.NewUserGetByIDOK().WithPayload(ret)
WithPayload functions form file *_responses.go is defined as
// WithPayload adds the payload to the user get by Id o k response
func (o *UserGetByIDOK) WithPayload(payload []*models.Users) *UserGetByIDOK {
o.Payload = payload
return o
}
I am attempting to create a REST API in Go. I have it partially working in that it will return 4 separate json objects like such:
[{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps":""},
{"Name":"QA1","Server":"","Description":"","Apps":"Duo"},
{"Name":"QA1","Server":"","Description":"","Apps":"Git"},
{"Name":"QA1","Server":"","Description":"","Apps":"php"}]
What I want is a single returned object like:
[{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps": "Duo|Git|php"}]
I obviously have the way that I am either making my queries or the structs (or both or something else) not quite correct. I want to make sure I understand how to do this right because I would like to expand on it for other queries and such down the road. I have included the "full" go code below.
To be clear, I'm not simply looking for the solution (though I would of course appreciate that to compare with), but where I've gone wrong in my thinking and what the correct approach would be.
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io/ioutil"
"log"
"net/http"
)
// There can be zero or more apps on a volume
type Apps struct {
Name string
}
// Volumes have a name, description, are on a server and have multiple services/apps
type Volume struct {
Name string
Server string
Description string
Services Apps
}
//Handle all requests
func Handler(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-type", "text/html")
webpage, err := ioutil.ReadFile("index.html")
if err != nil {
http.Error(response, fmt.Sprintf("home.html file error %v", err), 500)
}
fmt.Fprint(response, string(webpage))
}
// DB Connection
const (
DB_HOST = "mydbhost"
DB_NAME = "mydb"
DB_USER = "mydbuser"
DB_PASS = "mydbpass"
)
// Respond to URLs of the form /api
func APIHandler(response http.ResponseWriter, request *http.Request) {
//Connect to database
dsn := DB_USER + ":" + DB_PASS + "#" + DB_HOST + "/" + DB_NAME + "?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Println(err.Error())
}
defer db.Close()
// Open doesn't open a connection. Validate DSN data:
err = db.Ping()
if err != nil {
fmt.Println(err.Error())
}
//set mime type to JSON
response.Header().Set("Content-type", "application/json")
result := []*Volume{}
switch request.Method {
case "GET":
srvrnm := request.URL.Query().Get("srvrnm")
appnm := request.URL.Query().Get("appnm")
srvrs, err := db.Prepare("select VOLUMES.name as volnm, SERVERS.name as srvrnm, VOLUMES.description as descr From VOLUMES LEFT JOIN SERVERS ON VOLUMES.server_id = SERVERS.id where SERVERS.name = ?")
if err != nil {
fmt.Print(err)
}
srvcs, err := db.Prepare("select VOLUMES.name as volnm, SUPPRTSVCS.name as app_name From VOLUMES as VOLUMES JOIN HOSTSVCS ON VOLUMES.id = HOSTSVCS.volume_id JOIN SUPPRTSVCS ON SUPPRTSVCS.id = HOSTSVCS.supportsvcs_id where VOLUMES.name = ?")
if err != nil {
fmt.Print(err)
}
// Run the SQL Query to Get Volum & Description From Hostname
srvrrows, err := srvrs.Query(srvrnm)
if err != nil {
fmt.Print(err)
}
for srvrrows.Next() {
var volnm string
var srvrnm string
var descr string
// Scan the First Query
err = srvrrows.Scan(&volnm, &srvrnm, &descr)
if err != nil {
fmt.Println("Error scanning: " + err.Error())
return
}
// Append Slice with results from the scan
result = append(result, &Volume{Name: volnm, Server: srvrnm, Description: descr})
}
// Run the SQL Query for Services/Apps
srvcrows, err := srvcs.Query(appnm)
if err != nil {
fmt.Print(err)
}
for srvcrows.Next() {
var volnm string
var appnm string
// Scan the Second Query
err = srvcrows.Scan(&volnm, &appnm)
if err != nil {
fmt.Println("Error scanning: " + err.Error())
return
}
// Append Slice with results from the scan
result = append(result, &Volume{Name: volnm, Apps: appnm})
}
default:
}
json, err := json.Marshal(result)
if err != nil {
fmt.Println(err)
return
}
fmt.Fprintf(response, string(json))
db.Close()
}
func main() {
port := "1236"
var err string
mux := http.NewServeMux()
mux.Handle("/api", http.HandlerFunc(APIHandler))
mux.Handle("/", http.HandlerFunc(Handler))
// Start listing on a given port with these routes on this server.
log.Print("Listening on port " + port + " ... ")
errs := http.ListenAndServe(":"+port, mux)
if errs != nil {
log.Fatal("ListenAndServe error: ", err)
}
}
From the sounds of it, you want to your result to look like:
[
{"Name":"QA1","Server":"BOT1","Description":"Tools","Apps": ["Duo","Git","php"]
]
Hence you want your Volumes struct to look like:
type Volume struct {
Name string
Server string
Description string
Services []Apps
}
If you want the Apps to actually output Duo|Git|php then you could create a custom type instead of []Apps with a JSON Marshaler implementation. This could simply return json.Marshal(strings.join(names,"|"))
Rather than run two separate queries, it would be more efficient to run a single query that selects the product of volumes & apps together. It is important that this query is sorted by volume so all volume rows are contiguous. Example query output would be:
Name | Server | Desc | App
---- | ------ | ----- | ---
Vol1 | Srv1 | Desc1 | App1
Vol1 | Srv1 | Desc1 | App2
Vol2 | Srv2 | Desc2 | App3
You would then loop over this and detect if you are looking at a new volume. If so, create a new entry in the result. If not, add the App to the list of apps. For example:
var (
volnm string
srvrnm string
descr string
appnm string
v *Volume
result []*Volume
)
for srvrrows.Next() {
if err = srvcrows.Scan(&volnm, &srvrnm, &descr, &appnm);err!=nil {
// Handle error
}
// Add App to current volume if same, otherwise start a new volume
if v!=nil && v.Name == volnm {
v.Services = append(v.Services,Apps{appnm})
} else {
v = &Volume{
Name: volnm,
Server: svrnm,
Description: descr,
Services: []Apps{appnm}}
result = append(result,v)
}
}
// Finished, return result etc...
When taking this approach, you need an appropriate parent record discriminator. I'd just used v.Name == volnm for illustration purposes but this should really be checking the primary key. You can make this an unexported (lowercase) field in the struct if you do not wish to export it through the API.
Session Variables are not maintained across request while using gorilla sessions web toolkit.
When I start the server and type localhost:8100/ page is directed to login.html since session values do not exist.After I login I set the session variable in the store and the page is redirected to home.html. But when I open a new tab and type localhost:8100/ the page should be directed to home.html using already stored session variables, but the page is instead redirected to login.html.
Following is the code.
package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gocql/gocql"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"net/http"
"time"
)
var store = sessions.NewCookieStore([]byte("something-very-secret"))
var router = mux.NewRouter()
func init() {
store.Options = &sessions.Options{
Domain: "localhost",
Path: "/",
MaxAge: 3600 * 1, // 1 hour
HttpOnly: true,
}
}
func main() {
//session handling
router.HandleFunc("/", SessionHandler)
router.HandleFunc("/signIn", SignInHandler)
router.HandleFunc("/signUp", SignUpHandler)
router.HandleFunc("/logOut", LogOutHandler)
http.Handle("/", router)
http.ListenAndServe(":8100", nil)
}
//handler for signIn
func SignInHandler(res http.ResponseWriter, req *http.Request) {
email := req.FormValue("email")
password := req.FormValue("password")
//Generate hash of password
hasher := md5.New()
hasher.Write([]byte(password))
encrypted_password := hex.EncodeToString(hasher.Sum(nil))
//cassandra connection
cluster := gocql.NewCluster("localhost")
cluster.Keyspace = "gbuy"
cluster.DefaultPort = 9042
cluster.Consistency = gocql.Quorum
session, _ := cluster.CreateSession()
defer session.Close()
//select query
var firstname string
stmt := "SELECT firstname FROM USER WHERE email= '" + email + "' and password ='" + encrypted_password + "';"
err := session.Query(stmt).Scan(&firstname)
if err != nil {
fmt.Fprintf(res, "failed")
} else {
if firstname == "" {
fmt.Fprintf(res, "failed")
} else {
fmt.Fprintf(res, firstname)
}
}
//store in session variable
sessionNew, _ := store.Get(req, "loginSession")
// Set some session values.
sessionNew.Values["email"] = email
sessionNew.Values["name"] = firstname
// Save it.
sessionNew.Save(req, res)
//store.Save(req,res,sessionNew)
fmt.Println("Session after logging:")
fmt.Println(sessionNew)
}
//handler for signUp
func SignUpHandler(res http.ResponseWriter, req *http.Request) {
fName := req.FormValue("fName")
lName := req.FormValue("lName")
email := req.FormValue("email")
password := req.FormValue("passwd")
birthdate := req.FormValue("date")
city := req.FormValue("city")
gender := req.FormValue("gender")
//Get current timestamp and format it.
sysdate := time.Now().Format("2006-01-02 15:04:05-0700")
//Generate hash of password
hasher := md5.New()
hasher.Write([]byte(password))
encrypted_password := hex.EncodeToString(hasher.Sum(nil))
//cassandra connection
cluster := gocql.NewCluster("localhost")
cluster.Keyspace = "gbuy"
cluster.DefaultPort = 9042
cluster.Consistency = gocql.Quorum
session, _ := cluster.CreateSession()
defer session.Close()
//Insert the data into the Table
stmt := "INSERT INTO USER (email,firstname,lastname,birthdate,city,gender,password,creation_date) VALUES ('" + email + "','" + fName + "','" + lName + "','" + birthdate + "','" + city + "','" + gender + "','" + encrypted_password + "','" + sysdate + "');"
fmt.Println(stmt)
err := session.Query(stmt).Exec()
if err != nil {
fmt.Fprintf(res, "failed")
} else {
fmt.Fprintf(res, fName)
}
}
//handler for logOut
func LogOutHandler(res http.ResponseWriter, req *http.Request) {
sessionOld, err := store.Get(req, "loginSession")
fmt.Println("Session in logout")
fmt.Println(sessionOld)
if err = sessionOld.Save(req, res); err != nil {
fmt.Println("Error saving session: %v", err)
}
}
//handler for Session
func SessionHandler(res http.ResponseWriter, req *http.Request) {
router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
session, _ := store.Get(req, "loginSession")
fmt.Println("Session in SessionHandler")
fmt.Println(session)
if val, ok := session.Values["email"].(string); ok {
// if val is a string
switch val {
case "": {
http.Redirect(res, req, "html/login.html", http.StatusFound) }
default:
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
} else {
// if val is not a string type
http.Redirect(res, req, "html/login.html", http.StatusFound)
}
}
Can somebody tell me what I am doing wrong. Thanks in advance.
First up: you should never, ever, use md5 to hash passwords. Read this article on why, and then use Go's bcrypt package. You should also parameterise your SQL queries else you are open to catastrophic SQL injection attacks.
Anyway: there are a few problems you need to address here:
Your sessions aren't "sticking" is that you're setting the Path as /loginSession - so when a user visits any other path (i.e. /), the session isn't valid for that scope.
You should be setting up a session store on program initialisation and setting the options there:
var store = sessions.NewCookieStore([]byte("something-very-secret"))
func init() {
store.Options = &sessions.Options{
Domain: "localhost",
Path: "/",
MaxAge: 3600 * 8, // 8 hours
HttpOnly: true,
}
The reason you might set a more specific path is if logged in users are always within a sub-route like /accounts. In your case, that's not what's happening.
I should add that Chrome's "Resource" tab in the Web Inspector (Resources > Cookies) is incredibly useful for debugging issues like these as you can see the cookie expiry, path and other settings.
You're also checking session.Values["email"] == nil, which doesn't work. An empty string in Go is just "", and because session.Values is a map[string]interface{}, you need to type assert the value to a string:
i.e.
if val, ok := session.Values["email"].(string); ok {
// if val is a string
switch val {
case "":
http.Redirect(res, req, "html/login.html", http.StatusFound)
default:
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
} else {
// if val is not a string type
http.Redirect(res, req, "html/login.html", http.StatusFound)
}
We deal with the "not a string" case so we're explicit about what the program should do if the session is not how we expected (client modified it, or an older version of our program used a different type).
You are not checking errors when saving your sessions.
sessionNew.Save(req, res)
... should be:
err := sessionNew.Save(req, res)
if err != nil {
// handle the error case
}
You should get/validate the session in SessionHandler before serving static files (you are doing it in a very roundabout way, however):
func SessionHandler(res http.ResponseWriter, req *http.Request) {
session, err := store.Get(req, "loginSession")
if err != nil {
// Handle the error
}
if session.Values["email"] == nil {
http.Redirect(res, req, "html/login.html", http.StatusFound)
} else {
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
// This shouldn't be here - router isn't scoped in this function! You should set this in your main() and wrap it with a function that checks for a valid session.
router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
}
The problem is you're writing to the response before calling session.Save. That prevents the headers from being written and thus your cookie from being sent to the client.
In the code after session.Query you're calling Fprintf on the response, as soon as this code executes, calling sessionNew.Save essentially does nothing. Remove any code that writes to the response and try again.
I guess gorilla toolkit's session ought to return an error when calling Save if the response has already been written to.
Following on from the comment chain, please try removing the Domain constraint from the session options, or replace it with a FQDN that resolves (using /etc/hosts for example).
This appears to be a bug in Chromium where cookies with an explicit 'localhost' domain aren't sent. The issue doesn't seem to present itself in Firefox.
I was able to get your demo working using
store.Options = &sessions.Options{
// Domain: "localhost",
MaxAge: 3600 * 1, // 1 hour
HttpOnly: true,
}
In my case the problem was the Path. I know the question is not about it, but this post appears first when you search Google. So, I was starting the session in a path like:
/usuario/login
So the path was set to /usuario, and then, when I made another requests from / the cookie was not set because / is not same as /usuario
I fixed it by specifying a Path, i know this should be obvious but took me some hours to realize it. So:
&sessions.Options{
MaxAge: 60 * 60 * 24,
HttpOnly: true,
Path: "/", // <-- This is very important
}
More info about general cookies: https://developer.mozilla.org/es/docs/Web/HTTP/Cookies
Use a server side "FilesystemStore" instead of a "CookieStore" to save the session variables. Another alternative would be to update the session as a context variable for the request i.e., store the session in the context and let the browser pass it around in every request, using the context.Set() from the gorilla/context package.
Using "CookieStore" is heavy for the client because as the amount of information stored in the cookie grows, more information is transmitted over the wire for every request and response. The advantage it serves is that there is no need to store the session information on the server side. If it is not a constraint to store session information on the server, the ideal way should be to store login and authentication related information on a server side "non-cookie" session store and just pass a token to the client. The server would maintain a map of the token and session information. The "FilesystemStore" allows you to do this.
Though both the "FilesystemStore" and "CookieStore" implement the "Store" interface, each of their "Save()" function's implementations are slightly different. The source code for both the functions, CookieStore.Save() and FilesystemStore.Save() will help us understand why "CookieStore" is not able to persist the session information. The FilesystemStore's Save() method apart from writing the session information to the response header, also saves the information on the server side session file. In a "CookieStore" implementation, if the browser is not able to send the new modified cookie from a response to the next request, the request might fail. In a "FilesystemStore" implementation, the token that is given to the browser always remains the same. The session information is updated in a file and is fetched based on the requesting token, whenever required.