pgweb/pkg/api/api.go

609 lines
13 KiB
Go
Raw Normal View History

2015-04-30 19:47:07 +03:00
package api
2014-10-11 02:14:17 +04:00
import (
2022-01-08 23:45:21 +03:00
"context"
"encoding/base64"
2014-10-11 02:14:17 +04:00
"fmt"
"net/http"
2016-11-06 06:23:26 +03:00
neturl "net/url"
2017-09-17 04:32:41 +03:00
"strings"
"time"
2014-11-11 08:10:05 +03:00
"github.com/gin-gonic/gin"
"github.com/tuvistavie/securerandom"
2015-04-30 19:47:07 +03:00
"github.com/sosedoff/pgweb/pkg/bookmarks"
"github.com/sosedoff/pgweb/pkg/client"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/metrics"
"github.com/sosedoff/pgweb/pkg/shared"
2021-08-10 20:12:05 +03:00
"github.com/sosedoff/pgweb/static"
2014-10-11 02:14:17 +04:00
)
var (
2018-12-01 06:40:28 +03:00
// DbClient represents the active database connection in a single-session mode
DbClient *client.Client
// DbSessions represents the mapping for client connections
2022-12-02 22:36:31 +03:00
DbSessions *SessionManager
)
2018-12-01 06:40:28 +03:00
// DB returns a database connection from the client context
func DB(c *gin.Context) *client.Client {
if command.Opts.Sessions {
2022-12-02 22:36:31 +03:00
return DbSessions.Get(getSessionId(c.Request))
}
return DbClient
}
// setClient sets the database client connection for the sessions
func setClient(c *gin.Context, newClient *client.Client) error {
currentClient := DB(c)
if currentClient != nil {
currentClient.Close()
}
if !command.Opts.Sessions {
DbClient = newClient
return nil
}
sid := getSessionId(c.Request)
if sid == "" {
return errSessionRequired
}
2022-12-02 22:36:31 +03:00
DbSessions.Add(sid, newClient)
return nil
}
2015-04-30 19:47:07 +03:00
// GetHome renders the home page
2021-08-10 20:12:05 +03:00
func GetHome(prefix string) http.Handler {
if prefix != "" {
prefix = "/" + prefix
}
return http.StripPrefix(prefix, static.GetHandler())
2015-05-03 04:10:14 +03:00
}
2014-10-13 23:40:56 +04:00
2021-08-10 20:12:05 +03:00
func GetAssets(prefix string) http.Handler {
if prefix != "" {
prefix = "/" + prefix + "static/"
} else {
prefix = "/static/"
}
return http.StripPrefix(prefix, static.GetHandler())
2014-10-13 22:55:19 +04:00
}
// GetSessions renders the number of active sessions
func GetSessions(c *gin.Context) {
// In debug mode endpoint will return a lot of sensitive information
// like full database connection string and all query history.
if command.Opts.Debug {
2022-12-02 22:36:31 +03:00
successResponse(c, DbSessions.Sessions())
return
}
2022-12-02 22:36:31 +03:00
successResponse(c, gin.H{"sessions": DbSessions.Len()})
}
// ConnectWithBackend creates a new connection based on backend resource
func ConnectWithBackend(c *gin.Context) {
// Setup a new backend client
backend := Backend{
Endpoint: command.Opts.ConnectBackend,
Token: command.Opts.ConnectToken,
2022-12-02 20:45:23 +03:00
PassHeaders: strings.Split(command.Opts.ConnectHeaders, ","),
}
2022-01-08 23:45:21 +03:00
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// Fetch connection credentials
2022-01-08 23:45:21 +03:00
cred, err := backend.FetchCredential(ctx, c.Param("resource"), c)
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
// Make the new session
sid, err := securerandom.Uuid()
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
c.Request.Header.Add("x-session-id", sid)
// Connect to the database
2018-12-02 07:35:11 +03:00
cl, err := client.NewFromUrl(cred.DatabaseURL, nil)
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
cl.External = true
// Finalize session seetup
_, err = cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
redirectURI := fmt.Sprintf("/%s?session=%s", command.Opts.Prefix, sid)
c.Redirect(302, redirectURI)
}
// Connect creates a new client connection
2015-05-03 04:13:04 +03:00
func Connect(c *gin.Context) {
if command.Opts.LockSession {
badRequest(c, errSessionLocked)
return
}
var (
cl *client.Client
err error
)
if bookmarkID := c.Request.FormValue("bookmark_id"); bookmarkID != "" {
cl, err = ConnectWithBookmark(bookmarkID)
} else {
cl, err = ConnectWithURL(c)
}
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
2015-04-30 19:47:07 +03:00
err = cl.Test()
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
2015-04-30 19:47:07 +03:00
info, err := cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
2018-12-01 06:40:28 +03:00
successResponse(c, info.Format()[0])
}
func ConnectWithURL(c *gin.Context) (*client.Client, error) {
url := c.Request.FormValue("url")
if url == "" {
return nil, errURLRequired
}
url, err := connection.FormatURL(command.Options{
URL: url,
Passfile: command.Opts.Passfile,
})
if err != nil {
return nil, err
}
var sshInfo *shared.SSHInfo
if c.Request.FormValue("ssh") != "" {
sshInfo = parseSshInfo(c)
}
return client.NewFromUrl(url, sshInfo)
}
func ConnectWithBookmark(id string) (*client.Client, error) {
manager := bookmarks.NewManager(command.Opts.BookmarksDir)
bookmark, err := manager.Get(id)
if err != nil {
return nil, err
}
return client.NewFromBookmark(bookmark)
}
// SwitchDb perform database switch for the client connection
2016-11-06 06:23:26 +03:00
func SwitchDb(c *gin.Context) {
if command.Opts.LockSession {
badRequest(c, errSessionLocked)
2016-11-06 06:23:26 +03:00
return
}
name := c.Request.URL.Query().Get("db")
if name == "" {
name = c.Request.FormValue("db")
}
2016-11-06 06:23:26 +03:00
if name == "" {
badRequest(c, errDatabaseNameRequired)
2016-11-06 06:23:26 +03:00
return
}
conn := DB(c)
if conn == nil {
badRequest(c, errNotConnected)
2016-11-06 06:23:26 +03:00
return
}
// Do not allow switching databases for connections from third-party backends
if conn.External {
badRequest(c, errSessionLocked)
return
}
currentURL, err := neturl.Parse(conn.ConnectionString)
2016-11-06 06:23:26 +03:00
if err != nil {
badRequest(c, errInvalidConnString)
2016-11-06 06:23:26 +03:00
return
}
currentURL.Path = name
2016-11-06 06:23:26 +03:00
cl, err := client.NewFromUrl(currentURL.String(), nil)
2016-11-06 06:23:26 +03:00
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2016-11-06 06:23:26 +03:00
return
}
err = cl.Test()
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2016-11-06 06:23:26 +03:00
return
}
info, err := cl.Info()
if err == nil {
err = setClient(c, cl)
}
if err != nil {
cl.Close()
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
2016-11-06 06:23:26 +03:00
}
conn.Close()
2018-12-01 06:40:28 +03:00
successResponse(c, info.Format()[0])
2016-11-06 06:23:26 +03:00
}
// Disconnect closes the current database connection
2016-02-05 08:05:42 +03:00
func Disconnect(c *gin.Context) {
if command.Opts.LockSession {
badRequest(c, errSessionLocked)
return
}
if command.Opts.Sessions {
result := DbSessions.Remove(getSessionId(c.Request))
successResponse(c, gin.H{"success": result})
return
}
2016-02-05 08:05:42 +03:00
conn := DB(c)
if conn == nil {
badRequest(c, errNotConnected)
2016-02-05 08:05:42 +03:00
return
}
err := conn.Close()
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2016-02-05 08:05:42 +03:00
return
}
2018-12-01 06:40:28 +03:00
successResponse(c, gin.H{"success": true})
2016-02-05 08:05:42 +03:00
}
// RunQuery executes the query
2015-05-03 04:13:04 +03:00
func RunQuery(c *gin.Context) {
query := cleanQuery(c.Request.FormValue("query"))
2014-10-11 02:14:17 +04:00
if query == "" {
badRequest(c, errQueryRequired)
2014-10-11 02:14:17 +04:00
return
}
2015-05-03 04:32:16 +03:00
HandleQuery(query, c)
2014-10-11 02:14:17 +04:00
}
// ExplainQuery renders query explain plan
2015-05-03 04:13:04 +03:00
func ExplainQuery(c *gin.Context) {
query := cleanQuery(c.Request.FormValue("query"))
2014-10-11 22:24:12 +04:00
if query == "" {
badRequest(c, errQueryRequired)
2014-10-11 22:24:12 +04:00
return
}
HandleQuery(fmt.Sprintf("EXPLAIN %s", query), c)
}
// AnalyzeQuery renders query explain plan and analyze profile
func AnalyzeQuery(c *gin.Context) {
query := cleanQuery(c.Request.FormValue("query"))
if query == "" {
badRequest(c, errQueryRequired)
return
}
2015-05-03 04:32:16 +03:00
HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
2014-10-11 22:24:12 +04:00
}
// GetDatabases renders a list of all databases on the server
func GetDatabases(c *gin.Context) {
if command.Opts.LockSession {
serveResult(c, []string{}, nil)
return
}
conn := DB(c)
if conn.External {
errorResponse(c, 403, errNotPermitted)
return
}
names, err := DB(c).Databases()
serveResult(c, names, err)
}
// GetObjects renders a list of database objects
func GetObjects(c *gin.Context) {
result, err := DB(c).Objects()
if err != nil {
badRequest(c, err)
return
}
successResponse(c, client.ObjectsFromResult(result))
}
// GetSchemas renders list of available schemas
2015-05-03 04:13:04 +03:00
func GetSchemas(c *gin.Context) {
2016-01-13 06:33:44 +03:00
res, err := DB(c).Schemas()
2018-12-01 06:40:28 +03:00
serveResult(c, res, err)
2014-10-11 02:14:17 +04:00
}
// GetTable renders table information
2015-05-03 04:13:04 +03:00
func GetTable(c *gin.Context) {
var (
res *client.Result
err error
)
db := DB(c)
tableName := c.Params.ByName("table")
switch c.Request.FormValue("type") {
case client.ObjTypeMaterializedView:
res, err = db.MaterializedView(tableName)
case client.ObjTypeFunction:
res, err = db.Function(tableName)
default:
res, err = db.Table(tableName)
}
2018-12-01 06:40:28 +03:00
serveResult(c, res, err)
2014-10-11 02:14:17 +04:00
}
// GetTableRows renders table rows
2015-05-03 04:13:04 +03:00
func GetTableRows(c *gin.Context) {
offset, err := parseIntFormValue(c, "offset", 0)
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
limit, err := parseIntFormValue(c, "limit", 100)
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
2015-04-30 19:47:07 +03:00
opts := client.RowsOptions{
Limit: limit,
Offset: offset,
SortColumn: c.Request.FormValue("sort_column"),
SortOrder: c.Request.FormValue("sort_order"),
Where: c.Request.FormValue("where"),
}
res, err := DB(c).TableRows(c.Params.ByName("table"), opts)
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
countRes, err := DB(c).TableRowsCount(c.Params.ByName("table"), opts)
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
return
}
numFetch := int64(opts.Limit)
numOffset := int64(opts.Offset)
numRows := countRes.Rows[0][0].(int64)
numPages := numRows / numFetch
if numPages*numFetch < numRows {
numPages++
}
res.Pagination = &client.Pagination{
Rows: numRows,
Page: (numOffset / numFetch) + 1,
Pages: numPages,
PerPage: numFetch,
}
2018-12-01 06:40:28 +03:00
serveResult(c, res, err)
}
// GetTableInfo renders a selected table information
2015-05-03 04:13:04 +03:00
func GetTableInfo(c *gin.Context) {
res, err := DB(c).TableInfo(c.Params.ByName("table"))
2018-12-01 06:40:28 +03:00
if err == nil {
successResponse(c, res.Format()[0])
} else {
badRequest(c, err)
}
}
// GetHistory renders a list of recent queries
2015-05-01 03:59:48 +03:00
func GetHistory(c *gin.Context) {
2018-12-01 06:40:28 +03:00
successResponse(c, DB(c).History)
2014-10-11 02:14:17 +04:00
}
// GetConnectionInfo renders information about current connection
2015-05-01 03:59:48 +03:00
func GetConnectionInfo(c *gin.Context) {
conn := DB(c)
if err := conn.TestWithTimeout(5 * time.Second); err != nil {
badRequest(c, err)
return
}
2014-10-11 02:14:17 +04:00
res, err := conn.Info()
2014-10-11 02:14:17 +04:00
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2014-10-11 02:14:17 +04:00
return
}
info := res.Format()[0]
info["session_lock"] = command.Opts.LockSession
2018-12-01 06:40:28 +03:00
successResponse(c, info)
2014-10-11 02:14:17 +04:00
}
// GetActivity renders a list of running queries
2015-05-01 03:59:48 +03:00
func GetActivity(c *gin.Context) {
res, err := DB(c).Activity()
2018-12-01 06:40:28 +03:00
serveResult(c, res, err)
}
// GetTableIndexes renders a list of database table indexes
2015-05-01 03:59:48 +03:00
func GetTableIndexes(c *gin.Context) {
res, err := DB(c).TableIndexes(c.Params.ByName("table"))
2018-12-01 06:40:28 +03:00
serveResult(c, res, err)
2014-10-11 22:20:16 +04:00
}
// GetTableConstraints renders a list of database constraints
2015-12-05 03:14:03 +03:00
func GetTableConstraints(c *gin.Context) {
res, err := DB(c).TableConstraints(c.Params.ByName("table"))
2018-12-01 06:40:28 +03:00
serveResult(c, res, err)
2015-12-05 03:14:03 +03:00
}
// GetTablesStats renders data sizes and estimated rows for all tables in the database
func GetTablesStats(c *gin.Context) {
res, err := DB(c).TablesStats()
serveResult(c, res, err)
}
// HandleQuery runs the database query
2015-05-03 04:32:16 +03:00
func HandleQuery(query string, c *gin.Context) {
metrics.IncrementQueriesCount()
rawQuery, err := base64.StdEncoding.DecodeString(desanitize64(query))
if err == nil {
query = string(rawQuery)
}
2014-10-11 02:14:17 +04:00
result, err := DB(c).Query(query)
2014-10-11 02:14:17 +04:00
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2014-10-11 02:14:17 +04:00
return
}
format := getQueryParam(c, "format")
filename := getQueryParam(c, "filename")
if filename == "" {
filename = fmt.Sprintf("pgweb-%v.%v", time.Now().Unix(), format)
}
if format != "" {
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
}
switch format {
case "csv":
c.Data(200, "text/csv", result.CSV())
case "json":
c.Data(200, "application/json", result.JSON())
case "xml":
c.XML(200, result)
default:
c.JSON(200, result)
}
2014-10-11 02:14:17 +04:00
}
2014-10-13 23:40:56 +04:00
// GetBookmarks renders the list of available bookmarks
2015-05-01 03:59:48 +03:00
func GetBookmarks(c *gin.Context) {
manager := bookmarks.NewManager(command.Opts.BookmarksDir)
ids, err := manager.ListIDs()
serveResult(c, ids, err)
2014-12-03 07:19:38 +03:00
}
// GetInfo renders the pgweb system information
func GetInfo(c *gin.Context) {
successResponse(c, gin.H{
"app": command.Info,
"features": gin.H{
"session_lock": command.Opts.LockSession,
"query_timeout": command.Opts.QueryTimeout,
},
})
}
2017-09-17 04:32:41 +03:00
// DataExport performs database table export
2017-09-17 04:32:41 +03:00
func DataExport(c *gin.Context) {
db := DB(c)
info, err := db.Info()
if err != nil {
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2017-09-17 04:32:41 +03:00
return
}
dump := client.Dump{
Table: strings.TrimSpace(c.Request.FormValue("table")),
}
// Perform validation of pg_dump command availability and compatibility.
// Must be done before the actual command is executed to display errors.
if err := dump.Validate(db.ServerVersion()); err != nil {
badRequest(c, err)
return
}
2017-09-17 04:32:41 +03:00
formattedInfo := info.Format()[0]
filename := formattedInfo["current_database"].(string)
if dump.Table != "" {
filename = filename + "_" + dump.Table
}
filename = sanitizeFilename(filename)
filename = fmt.Sprintf("%s_%s", filename, time.Now().Format("20060102_150405"))
2017-09-17 04:32:41 +03:00
2018-12-01 06:40:28 +03:00
c.Header(
"Content-Disposition",
fmt.Sprintf(`attachment; filename="%s.sql.gz"`, filename),
2018-12-01 06:40:28 +03:00
)
2017-09-17 04:32:41 +03:00
err = dump.Export(c.Request.Context(), db.ConnectionString, c.Writer)
2017-09-17 04:32:41 +03:00
if err != nil {
logger.WithError(err).Error("pg_dump command failed")
2018-12-01 06:40:28 +03:00
badRequest(c, err)
2017-09-17 04:32:41 +03:00
}
}
// GetFunction renders function information
func GetFunction(c *gin.Context) {
res, err := DB(c).Function(c.Param("id"))
serveResult(c, res, err)
}