blockbook/server/internal.go

275 lines
8.8 KiB
Go

package server
import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/golang/glog"
"github.com/juju/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/trezor/blockbook/api"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/fiat"
)
// InternalServer is handle to internal http server
type InternalServer struct {
htmlTemplates[InternalTemplateData]
https *http.Server
certFiles string
db *db.RocksDB
txCache *db.TxCache
chain bchain.BlockChain
chainParser bchain.BlockChainParser
mempool bchain.Mempool
is *common.InternalState
api *api.Worker
}
// NewInternalServer creates new internal http interface to blockbook and returns its handle
func NewInternalServer(binding, certFiles string, db *db.RocksDB, chain bchain.BlockChain, mempool bchain.Mempool, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState, fiatRates *fiat.FiatRates) (*InternalServer, error) {
api, err := api.NewWorker(db, chain, mempool, txCache, metrics, is, fiatRates)
if err != nil {
return nil, err
}
addr, path := splitBinding(binding)
serveMux := http.NewServeMux()
https := &http.Server{
Addr: addr,
Handler: serveMux,
}
s := &InternalServer{
htmlTemplates: htmlTemplates[InternalTemplateData]{
debug: true,
},
https: https,
certFiles: certFiles,
db: db,
txCache: txCache,
chain: chain,
chainParser: chain.GetChainParser(),
mempool: mempool,
is: is,
api: api,
}
s.htmlTemplates.newTemplateData = s.newTemplateData
s.htmlTemplates.newTemplateDataWithError = s.newTemplateDataWithError
s.htmlTemplates.parseTemplates = s.parseTemplates
s.templates = s.parseTemplates()
serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
serveMux.Handle(path+"static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
serveMux.HandleFunc(path+"metrics", promhttp.Handler().ServeHTTP)
serveMux.HandleFunc(path, s.index)
serveMux.HandleFunc(path+"admin", s.htmlTemplateHandler(s.adminIndex))
serveMux.HandleFunc(path+"admin/ws-limit-exceeding-ips", s.htmlTemplateHandler(s.wsLimitExceedingIPs))
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
serveMux.HandleFunc(path+"admin/internal-data-errors", s.htmlTemplateHandler(s.internalDataErrors))
serveMux.HandleFunc(path+"admin/contract-info", s.htmlTemplateHandler(s.contractInfoPage))
serveMux.HandleFunc(path+"admin/contract-info/", s.jsonHandler(s.apiContractInfo, 0))
}
return s, nil
}
// Run starts the server
func (s *InternalServer) Run() error {
if s.certFiles == "" {
glog.Info("internal server: starting to listen on http://", s.https.Addr)
return s.https.ListenAndServe()
}
glog.Info("internal server: starting to listen on https://", s.https.Addr)
return s.https.ListenAndServeTLS(fmt.Sprint(s.certFiles, ".crt"), fmt.Sprint(s.certFiles, ".key"))
}
// Close closes the server
func (s *InternalServer) Close() error {
glog.Infof("internal server: closing")
return s.https.Close()
}
// Shutdown shuts down the server
func (s *InternalServer) Shutdown(ctx context.Context) error {
glog.Infof("internal server: shutdown")
return s.https.Shutdown(ctx)
}
func (s *InternalServer) index(w http.ResponseWriter, r *http.Request) {
si, err := s.api.GetSystemInfo(true)
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
buf, err := json.MarshalIndent(si, "", " ")
if err != nil {
glog.Error(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(buf)
}
const (
adminIndexTpl = iota + errorInternalTpl + 1
adminInternalErrorsTpl
adminLimitExceedingIPSTpl
adminContractInfoTpl
internalTplCount
)
// WsLimitExceedingIP is used to transfer data to the templates
type WsLimitExceedingIP struct {
IP string
Count int
}
// InternalTemplateData is used to transfer data to the templates
type InternalTemplateData struct {
CoinName string
CoinShortcut string
CoinLabel string
ChainType bchain.ChainType
Error *api.APIError
InternalDataErrors []db.BlockInternalDataError
RefetchingInternalData bool
WsGetAccountInfoLimit int
WsLimitExceedingIPs []WsLimitExceedingIP
}
func (s *InternalServer) newTemplateData(r *http.Request) *InternalTemplateData {
t := &InternalTemplateData{
CoinName: s.is.Coin,
CoinShortcut: s.is.CoinShortcut,
CoinLabel: s.is.CoinLabel,
ChainType: s.chainParser.GetChainType(),
}
return t
}
func (s *InternalServer) newTemplateDataWithError(error *api.APIError, r *http.Request) *InternalTemplateData {
td := s.newTemplateData(r)
td.Error = error
return td
}
func (s *InternalServer) parseTemplates() []*template.Template {
templateFuncMap := template.FuncMap{
"formatUint32": formatUint32,
}
createTemplate := func(filenames ...string) *template.Template {
if len(filenames) == 0 {
panic("Missing templates")
}
return template.Must(template.New(filepath.Base(filenames[0])).Funcs(templateFuncMap).ParseFiles(filenames...))
}
t := make([]*template.Template, internalTplCount)
t[errorTpl] = createTemplate("./static/internal_templates/error.html", "./static/internal_templates/base.html")
t[errorInternalTpl] = createTemplate("./static/internal_templates/error.html", "./static/internal_templates/base.html")
t[adminIndexTpl] = createTemplate("./static/internal_templates/index.html", "./static/internal_templates/base.html")
t[adminInternalErrorsTpl] = createTemplate("./static/internal_templates/block_internal_data_errors.html", "./static/internal_templates/base.html")
t[adminLimitExceedingIPSTpl] = createTemplate("./static/internal_templates/ws_limit_exceeding_ips.html", "./static/internal_templates/base.html")
t[adminContractInfoTpl] = createTemplate("./static/internal_templates/contract_info.html", "./static/internal_templates/base.html")
return t
}
func (s *InternalServer) adminIndex(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) {
data := s.newTemplateData(r)
return adminIndexTpl, data, nil
}
func (s *InternalServer) internalDataErrors(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) {
if r.Method == http.MethodPost {
err := s.api.RefetchInternalData()
if err != nil {
return errorTpl, nil, err
}
}
data := s.newTemplateData(r)
internalErrors, err := s.db.GetBlockInternalDataErrorsEthereumType()
if err != nil {
return errorTpl, nil, err
}
data.InternalDataErrors = internalErrors
data.RefetchingInternalData = s.api.IsRefetchingInternalData()
return adminInternalErrorsTpl, data, nil
}
func (s *InternalServer) wsLimitExceedingIPs(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) {
if r.Method == http.MethodPost {
s.is.ResetWsLimitExceedingIPs()
}
data := s.newTemplateData(r)
ips := make([]WsLimitExceedingIP, 0, len(s.is.WsLimitExceedingIPs))
for k, v := range s.is.WsLimitExceedingIPs {
ips = append(ips, WsLimitExceedingIP{k, v})
}
sort.Slice(ips, func(i, j int) bool {
return ips[i].Count > ips[j].Count
})
data.WsLimitExceedingIPs = ips
data.WsGetAccountInfoLimit = s.is.WsGetAccountInfoLimit
return adminLimitExceedingIPSTpl, data, nil
}
func (s *InternalServer) contractInfoPage(w http.ResponseWriter, r *http.Request) (tpl, *InternalTemplateData, error) {
data := s.newTemplateData(r)
return adminContractInfoTpl, data, nil
}
func (s *InternalServer) apiContractInfo(r *http.Request, apiVersion int) (interface{}, error) {
if r.Method == http.MethodPost {
return s.updateContracts(r)
}
var contractAddress string
i := strings.LastIndexByte(r.URL.Path, '/')
if i > 0 {
contractAddress = r.URL.Path[i+1:]
}
if len(contractAddress) == 0 {
return nil, api.NewAPIError("Missing contract address", true)
}
contractInfo, valid, err := s.api.GetContractInfo(contractAddress, bchain.UnknownTokenType)
if err != nil {
return nil, api.NewAPIError(err.Error(), true)
}
if !valid {
return nil, api.NewAPIError("Not a contract", true)
}
return contractInfo, nil
}
func (s *InternalServer) updateContracts(r *http.Request) (interface{}, error) {
data, err := io.ReadAll(r.Body)
if err != nil {
return nil, api.NewAPIError("Cannot get request body", true)
}
var contractInfos []bchain.ContractInfo
err = json.Unmarshal(data, &contractInfos)
if err != nil {
return nil, errors.Annotatef(err, "Cannot unmarshal body to array of ContractInfo objects")
}
for i := range contractInfos {
c := &contractInfos[i]
err := s.db.StoreContractInfo(c)
if err != nil {
return nil, api.NewAPIError("Error updating contract "+c.Contract+" "+err.Error(), true)
}
}
return "{\"success\":\"Updated " + strconv.Itoa(len(contractInfos)) + " contracts\"}", nil
}