|
|
|
@ -3,6 +3,7 @@
|
|
|
|
|
package http
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"encoding/json"
|
|
|
|
@ -23,6 +24,7 @@ import (
|
|
|
|
|
"github.com/rqlite/rqlite/auth"
|
|
|
|
|
"github.com/rqlite/rqlite/cluster"
|
|
|
|
|
"github.com/rqlite/rqlite/command"
|
|
|
|
|
"github.com/rqlite/rqlite/command/chunking"
|
|
|
|
|
"github.com/rqlite/rqlite/command/encoding"
|
|
|
|
|
"github.com/rqlite/rqlite/db"
|
|
|
|
|
"github.com/rqlite/rqlite/queue"
|
|
|
|
@ -58,8 +60,8 @@ type Database interface {
|
|
|
|
|
// an Execute or Query request.
|
|
|
|
|
Request(eqr *command.ExecuteQueryRequest) ([]*command.ExecuteQueryResponse, error)
|
|
|
|
|
|
|
|
|
|
// Load loads a SQLite file into the system
|
|
|
|
|
Load(lr *command.LoadRequest) error
|
|
|
|
|
// LoadChunk loads a SQLite database into the node, chunk by chunk.
|
|
|
|
|
LoadChunk(lc *command.LoadChunkRequest) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store is the interface the Raft-based database must implement.
|
|
|
|
@ -108,8 +110,8 @@ type Cluster interface {
|
|
|
|
|
// Backup retrieves a backup from a remote node and writes to the io.Writer.
|
|
|
|
|
Backup(br *command.BackupRequest, nodeAddr string, creds *cluster.Credentials, timeout time.Duration, w io.Writer) error
|
|
|
|
|
|
|
|
|
|
// Load loads a SQLite database into the node.
|
|
|
|
|
Load(lr *command.LoadRequest, nodeAddr string, creds *cluster.Credentials, timeout time.Duration) error
|
|
|
|
|
// LoadChunk loads a SQLite database into the node, chunk by chunk.
|
|
|
|
|
LoadChunk(lc *command.LoadChunkRequest, nodeAddr string, creds *cluster.Credentials, timeout time.Duration) error
|
|
|
|
|
|
|
|
|
|
// RemoveNode removes a node from the cluster.
|
|
|
|
|
RemoveNode(rn *command.RemoveNodeRequest, nodeAddr string, creds *cluster.Credentials, timeout time.Duration) error
|
|
|
|
@ -813,8 +815,7 @@ func (s *Service) handleBackup(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
s.lastBackup = time.Now()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleLoad loads the state contained in a .dump output. This API is different
|
|
|
|
|
// from others in that it expects a raw file, not wrapped in any kind of JSON.
|
|
|
|
|
// handleLoad loads the database from the given SQLite database file or SQLite dump.
|
|
|
|
|
func (s *Service) handleLoad(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !s.CheckRequestPerm(r, auth.PermLoad) {
|
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
@ -846,32 +847,39 @@ func (s *Service) handleLoad(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b, err := io.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
r.Body.Close()
|
|
|
|
|
|
|
|
|
|
if db.IsValidSQLiteData(b) {
|
|
|
|
|
s.logger.Printf("SQLite database file detected as load data")
|
|
|
|
|
lr := &command.LoadRequest{
|
|
|
|
|
Data: b,
|
|
|
|
|
// Peek at the incoming bytes so we can determine if this is a SQLite database
|
|
|
|
|
validSQLite := false
|
|
|
|
|
bufReader := bufio.NewReader(r.Body)
|
|
|
|
|
peek, err := bufReader.Peek(db.SQLiteHeaderSize)
|
|
|
|
|
if err == nil {
|
|
|
|
|
validSQLite = db.IsValidSQLiteData(peek)
|
|
|
|
|
if validSQLite {
|
|
|
|
|
s.logger.Printf("SQLite database file detected as load data")
|
|
|
|
|
if db.IsWALModeEnabled(peek) {
|
|
|
|
|
s.logger.Printf("SQLite database file is in WAL mode - rejecting load request")
|
|
|
|
|
http.Error(w, `SQLite database file is in WAL mode - convert it to DELETE mode via 'PRAGMA journal_mode=DELETE'`,
|
|
|
|
|
http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if db.IsWALModeEnabled(b) {
|
|
|
|
|
s.logger.Printf("SQLite database file is in WAL mode - rejecting load request")
|
|
|
|
|
http.Error(w, `SQLite database file is in WAL mode - convert it to DELETE mode via 'PRAGMA journal_mode=DELETE'`,
|
|
|
|
|
http.StatusBadRequest)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !validSQLite {
|
|
|
|
|
// Assume SQL text
|
|
|
|
|
b, err := io.ReadAll(bufReader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
r.Body.Close()
|
|
|
|
|
|
|
|
|
|
err := s.store.Load(lr)
|
|
|
|
|
if err != nil && err != store.ErrNotLeader {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
} else if err != nil && err == store.ErrNotLeader {
|
|
|
|
|
if redirect {
|
|
|
|
|
queries := []string{string(b)}
|
|
|
|
|
er := executeRequestFromStrings(queries, timings, false)
|
|
|
|
|
|
|
|
|
|
results, err := s.store.Execute(er)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == store.ErrNotLeader {
|
|
|
|
|
leaderAPIAddr := s.LeaderAPIAddr()
|
|
|
|
|
if leaderAPIAddr == "" {
|
|
|
|
|
stats.Add(numLeaderNotFound, 1)
|
|
|
|
@ -883,63 +891,75 @@ func (s *Service) handleLoad(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
http.Redirect(w, r, redirect, http.StatusMovedPermanently)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
resp.Error = err.Error()
|
|
|
|
|
} else {
|
|
|
|
|
resp.Results.ExecuteResult = results
|
|
|
|
|
}
|
|
|
|
|
resp.end = time.Now()
|
|
|
|
|
} else {
|
|
|
|
|
chunker := chunking.NewChunker(bufReader, 1024*1024)
|
|
|
|
|
|
|
|
|
|
addr, err := s.store.LeaderAddr()
|
|
|
|
|
for {
|
|
|
|
|
chunk, err := chunker.Next()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, fmt.Sprintf("leader address: %s", err.Error()),
|
|
|
|
|
http.StatusInternalServerError)
|
|
|
|
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if addr == "" {
|
|
|
|
|
stats.Add(numLeaderNotFound, 1)
|
|
|
|
|
http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable)
|
|
|
|
|
err = s.store.LoadChunk(chunk)
|
|
|
|
|
if err != nil && err != store.ErrNotLeader {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
username, password, ok := r.BasicAuth()
|
|
|
|
|
if !ok {
|
|
|
|
|
username = ""
|
|
|
|
|
}
|
|
|
|
|
} else if err != nil && err == store.ErrNotLeader {
|
|
|
|
|
if redirect {
|
|
|
|
|
leaderAPIAddr := s.LeaderAPIAddr()
|
|
|
|
|
if leaderAPIAddr == "" {
|
|
|
|
|
stats.Add(numLeaderNotFound, 1)
|
|
|
|
|
http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Add(ServedByHTTPHeader, addr)
|
|
|
|
|
loadErr := s.cluster.Load(lr, addr, makeCredentials(username, password), timeout)
|
|
|
|
|
if loadErr != nil {
|
|
|
|
|
if loadErr.Error() == "unauthorized" {
|
|
|
|
|
http.Error(w, "remote load not authorized", http.StatusUnauthorized)
|
|
|
|
|
} else {
|
|
|
|
|
http.Error(w, loadErr.Error(), http.StatusInternalServerError)
|
|
|
|
|
redirect := s.FormRedirect(r, leaderAPIAddr)
|
|
|
|
|
http.Redirect(w, r, redirect, http.StatusMovedPermanently)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
stats.Add(numRemoteLoads, 1)
|
|
|
|
|
// Allow this if block to exit, so response remains as before request
|
|
|
|
|
// forwarding was put in place.
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// No JSON structure expected for this API.
|
|
|
|
|
queries := []string{string(b)}
|
|
|
|
|
er := executeRequestFromStrings(queries, timings, false)
|
|
|
|
|
|
|
|
|
|
results, err := s.store.Execute(er)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == store.ErrNotLeader {
|
|
|
|
|
leaderAPIAddr := s.LeaderAPIAddr()
|
|
|
|
|
if leaderAPIAddr == "" {
|
|
|
|
|
addr, err := s.store.LeaderAddr()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, fmt.Sprintf("leader address: %s", err.Error()),
|
|
|
|
|
http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if addr == "" {
|
|
|
|
|
stats.Add(numLeaderNotFound, 1)
|
|
|
|
|
http.Error(w, ErrLeaderNotFound.Error(), http.StatusServiceUnavailable)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
redirect := s.FormRedirect(r, leaderAPIAddr)
|
|
|
|
|
http.Redirect(w, r, redirect, http.StatusMovedPermanently)
|
|
|
|
|
return
|
|
|
|
|
username, password, ok := r.BasicAuth()
|
|
|
|
|
if !ok {
|
|
|
|
|
username = ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Add(ServedByHTTPHeader, addr)
|
|
|
|
|
loadErr := s.cluster.LoadChunk(chunk, addr, makeCredentials(username, password), timeout)
|
|
|
|
|
if loadErr != nil {
|
|
|
|
|
if loadErr.Error() == "unauthorized" {
|
|
|
|
|
http.Error(w, "remote load not authorized", http.StatusUnauthorized)
|
|
|
|
|
} else {
|
|
|
|
|
http.Error(w, loadErr.Error(), http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
stats.Add(numRemoteLoads, 1)
|
|
|
|
|
// Allow this if block to exit, so response remains as before request
|
|
|
|
|
// forwarding was put in place.
|
|
|
|
|
}
|
|
|
|
|
if chunk.IsLast {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
resp.Error = err.Error()
|
|
|
|
|
} else {
|
|
|
|
|
resp.Results.ExecuteResult = results
|
|
|
|
|
}
|
|
|
|
|
resp.end = time.Now()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.writeResponse(w, r, resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|