github.com/decred/politeia@v1.4.0/politeiawww/legacy/politeiawww.go (about)

     1  // Copyright (c) 2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package legacy
     6  
     7  import (
     8  	"context"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"net/http"
    14  	"os"
    15  	"path/filepath"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/decred/dcrd/chaincfg/v3"
    20  	pd "github.com/decred/politeia/politeiad/api/v1"
    21  	pdv2 "github.com/decred/politeia/politeiad/api/v2"
    22  	pdclient "github.com/decred/politeia/politeiad/client"
    23  	cmplugin "github.com/decred/politeia/politeiad/plugins/comments"
    24  	piplugin "github.com/decred/politeia/politeiad/plugins/pi"
    25  	tkplugin "github.com/decred/politeia/politeiad/plugins/ticketvote"
    26  	umplugin "github.com/decred/politeia/politeiad/plugins/usermd"
    27  	"github.com/decred/politeia/politeiawww/config"
    28  	"github.com/decred/politeia/politeiawww/legacy/cmsdatabase"
    29  	database "github.com/decred/politeia/politeiawww/legacy/cmsdatabase"
    30  	cmsdb "github.com/decred/politeia/politeiawww/legacy/cmsdatabase/cockroachdb"
    31  	"github.com/decred/politeia/politeiawww/legacy/codetracker"
    32  	ghtracker "github.com/decred/politeia/politeiawww/legacy/codetracker/github"
    33  	"github.com/decred/politeia/politeiawww/legacy/comments"
    34  	"github.com/decred/politeia/politeiawww/legacy/events"
    35  	"github.com/decred/politeia/politeiawww/legacy/mail"
    36  	"github.com/decred/politeia/politeiawww/legacy/mdstream"
    37  	"github.com/decred/politeia/politeiawww/legacy/pi"
    38  	"github.com/decred/politeia/politeiawww/legacy/records"
    39  	"github.com/decred/politeia/politeiawww/legacy/sessions"
    40  	"github.com/decred/politeia/politeiawww/legacy/ticketvote"
    41  	"github.com/decred/politeia/politeiawww/legacy/user"
    42  	"github.com/decred/politeia/politeiawww/legacy/user/cockroachdb"
    43  	"github.com/decred/politeia/politeiawww/legacy/user/localdb"
    44  	"github.com/decred/politeia/politeiawww/legacy/user/mysql"
    45  	"github.com/decred/politeia/politeiawww/wsdcrdata"
    46  	"github.com/decred/politeia/util"
    47  	"github.com/google/uuid"
    48  	"github.com/gorilla/mux"
    49  	"github.com/robfig/cron"
    50  )
    51  
    52  // Politeiawww represents the legacy politeiawww server.
    53  type Politeiawww struct {
    54  	sync.RWMutex
    55  	cfg       *config.Config
    56  	params    *chaincfg.Params
    57  	router    *mux.Router // Public router
    58  	auth      *mux.Router // CSRF protected router
    59  	db        user.Database
    60  	sessions  *sessions.Sessions
    61  	mail      mail.Mailer
    62  	events    *events.Manager
    63  	http      *http.Client // Deprecated politeiad client
    64  	politeiad *pdclient.Client
    65  
    66  	// userEmails contains a mapping of all user emails to user ID.
    67  	// This is required for now because the email is stored as part of
    68  	// the encrypted user blob in the user database, but we also allow
    69  	// the user to sign in using their email address, requiring a user
    70  	// lookup by email. This is a temporary measure and should be
    71  	// removed once all user by email lookups have been taken out.
    72  	userEmails map[string]uuid.UUID // [email]userID
    73  
    74  	// The following fields are only used during piwww mode.
    75  	userPaywallPool map[uuid.UUID]paywallPoolMember // [userid][paywallPoolMember]
    76  
    77  	// The following fields are use only during cmswww mode.
    78  	cmsDB     cmsdatabase.Database
    79  	cron      *cron.Cron
    80  	wsDcrdata *wsdcrdata.Client
    81  	tracker   codetracker.CodeTracker
    82  
    83  	// The following fields are only used during testing.
    84  	test bool
    85  }
    86  
    87  // NewPoliteiawww returns a new legacy Politeiawww.
    88  func NewPoliteiawww(cfg *config.Config, router, auth *mux.Router, params *chaincfg.Params, pdclient *pdclient.Client) (*Politeiawww, error) {
    89  	// Setup http client for politeiad calls
    90  	httpClient, err := util.NewHTTPClient(false, cfg.RPCCert)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	// Setup user database
    96  	log.Infof("User database: %v", cfg.UserDB)
    97  
    98  	var userDB user.Database
    99  	var mailerDB user.MailerDB
   100  	switch cfg.UserDB {
   101  	case config.LevelDB:
   102  		db, err := localdb.New(cfg.DataDir)
   103  		if err != nil {
   104  			return nil, err
   105  		}
   106  		userDB = db
   107  
   108  	case config.MySQL, config.CockroachDB:
   109  		// If old encryption key is set it means that we need
   110  		// to open a db connection using the old key and then
   111  		// rotate keys.
   112  		var encryptionKey string
   113  		if cfg.OldEncryptionKey != "" {
   114  			encryptionKey = cfg.OldEncryptionKey
   115  		} else {
   116  			encryptionKey = cfg.EncryptionKey
   117  		}
   118  
   119  		// Open db connection.
   120  		network := filepath.Base(cfg.DataDir)
   121  		switch cfg.UserDB {
   122  		case config.MySQL:
   123  			mysql, err := mysql.New(cfg.DBHost,
   124  				cfg.DBPass, network, encryptionKey)
   125  			if err != nil {
   126  				return nil, fmt.Errorf("new mysql db: %v", err)
   127  			}
   128  			userDB = mysql
   129  			mailerDB = mysql
   130  		case config.CockroachDB:
   131  			cdb, err := cockroachdb.New(cfg.DBHost, network,
   132  				cfg.DBRootCert, cfg.DBCert, cfg.DBKey,
   133  				encryptionKey)
   134  			if err != nil {
   135  				return nil, fmt.Errorf("new cdb db: %v", err)
   136  			}
   137  			userDB = cdb
   138  			mailerDB = cdb
   139  		}
   140  
   141  		// Rotate keys.
   142  		if cfg.OldEncryptionKey != "" {
   143  			err = userDB.RotateKeys(cfg.EncryptionKey)
   144  			if err != nil {
   145  				return nil, fmt.Errorf("rotate userdb keys: %v", err)
   146  			}
   147  		}
   148  
   149  	default:
   150  		return nil, fmt.Errorf("invalid userdb '%v'", cfg.UserDB)
   151  	}
   152  
   153  	// Setup sessions store
   154  	var cookieKey []byte
   155  	if cookieKey, err = os.ReadFile(cfg.CookieKeyFile); err != nil {
   156  		log.Infof("Cookie key not found, generating one...")
   157  		cookieKey, err = util.Random(32)
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		err = os.WriteFile(cfg.CookieKeyFile, cookieKey, 0400)
   162  		if err != nil {
   163  			return nil, err
   164  		}
   165  		log.Infof("Cookie key generated")
   166  	}
   167  
   168  	// Setup mailer smtp client
   169  	mailer, err := mail.NewClient(cfg.MailHost, cfg.MailUser,
   170  		cfg.MailPass, cfg.MailAddress, cfg.MailCert,
   171  		cfg.MailSkipVerify, cfg.MailRateLimit, mailerDB)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("new mail client: %v", err)
   174  	}
   175  
   176  	// Setup legacy politeiawww context
   177  	p := &Politeiawww{
   178  		cfg:             cfg,
   179  		params:          params,
   180  		router:          router,
   181  		auth:            auth,
   182  		politeiad:       pdclient,
   183  		http:            httpClient,
   184  		db:              userDB,
   185  		mail:            mailer,
   186  		sessions:        sessions.New(userDB, cookieKey),
   187  		events:          events.NewManager(),
   188  		userEmails:      make(map[string]uuid.UUID, 1024),
   189  		userPaywallPool: make(map[uuid.UUID]paywallPoolMember, 1024),
   190  	}
   191  
   192  	err = p.setup()
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	return p, nil
   198  }
   199  
   200  // Close performs any required shutdown and cleanup for Politeiawww.
   201  func (p *Politeiawww) Close() {
   202  	// Close user db connection
   203  	p.db.Close()
   204  
   205  	// Perform application specific shutdown
   206  	switch p.cfg.Mode {
   207  	case config.PiWWWMode:
   208  		// Nothing to do
   209  	case config.CMSWWWMode:
   210  		p.wsDcrdata.Close()
   211  	}
   212  }
   213  
   214  // Setup performs any required setup for Politeiawww.
   215  func (p *Politeiawww) setup() error {
   216  	// Setup email-userID cache
   217  	err := p.initUserEmailsCache()
   218  	if err != nil {
   219  		return err
   220  	}
   221  
   222  	// Perform application specific setup
   223  	switch p.cfg.Mode {
   224  	case config.PiWWWMode:
   225  		return p.setupPi()
   226  	case config.CMSWWWMode:
   227  		return p.setupCMS()
   228  	default:
   229  		return fmt.Errorf("unknown mode: %v", p.cfg.Mode)
   230  	}
   231  }
   232  
   233  func (p *Politeiawww) setupPi() error {
   234  	// Get politeiad plugins
   235  	plugins, err := p.getPluginInventory()
   236  	if err != nil {
   237  		return fmt.Errorf("getPluginInventory: %v", err)
   238  	}
   239  
   240  	// Verify all required politeiad plugins have been registered
   241  	required := map[string]bool{
   242  		piplugin.PluginID: false,
   243  		cmplugin.PluginID: false,
   244  		tkplugin.PluginID: false,
   245  		umplugin.PluginID: false,
   246  	}
   247  	for _, v := range plugins {
   248  		_, ok := required[v.ID]
   249  		if !ok {
   250  			// Not a required plugin. Skip.
   251  			continue
   252  		}
   253  		required[v.ID] = true
   254  	}
   255  	notFound := make([]string, 0, len(required))
   256  	for pluginID, wasFound := range required {
   257  		if !wasFound {
   258  			notFound = append(notFound, pluginID)
   259  		}
   260  	}
   261  	if len(notFound) > 0 {
   262  		return fmt.Errorf("required politeiad plugins not found: %v", notFound)
   263  	}
   264  
   265  	// Setup api contexts
   266  	recordsCtx := records.New(p.cfg, p.politeiad, p.db, p.sessions, p.events)
   267  	commentsCtx, err := comments.New(p.cfg, p.politeiad, p.db,
   268  		p.sessions, p.events, plugins)
   269  	if err != nil {
   270  		return fmt.Errorf("new comments api: %v", err)
   271  	}
   272  	voteCtx, err := ticketvote.New(p.cfg, p.politeiad,
   273  		p.sessions, p.events, plugins)
   274  	if err != nil {
   275  		return fmt.Errorf("new ticketvote api: %v", err)
   276  	}
   277  	piCtx, err := pi.New(p.cfg, p.politeiad, p.db, p.mail,
   278  		p.sessions, p.events, plugins)
   279  	if err != nil {
   280  		return fmt.Errorf("new pi api: %v", err)
   281  	}
   282  
   283  	// Setup routes
   284  	p.setUserWWWRoutes()
   285  	p.setPiRoutes(recordsCtx, commentsCtx, voteCtx, piCtx)
   286  
   287  	// Verify paywall settings
   288  	switch {
   289  	case p.cfg.PaywallAmount != 0 && p.cfg.PaywallXpub != "":
   290  		// Paywall is enabled
   291  		paywallAmountInDcr := float64(p.cfg.PaywallAmount) / 1e8
   292  		log.Infof("Paywall : %v DCR", paywallAmountInDcr)
   293  
   294  	case p.cfg.PaywallAmount == 0 && p.cfg.PaywallXpub == "":
   295  		// Paywall is disabled
   296  		log.Infof("Paywall: DISABLED")
   297  
   298  	default:
   299  		// Invalid paywall setting
   300  		return fmt.Errorf("paywall settings invalid, both an amount " +
   301  			"and public key MUST be set")
   302  	}
   303  
   304  	// Setup paywall pool
   305  	p.userPaywallPool = make(map[uuid.UUID]paywallPoolMember)
   306  	err = p.initPaywallChecker()
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  func (p *Politeiawww) setupCMS() error {
   315  	// Setup routes
   316  	p.setCMSWWWRoutes()
   317  	p.setCMSUserWWWRoutes()
   318  
   319  	// Setup event manager
   320  	p.setupEventListenersCMS()
   321  
   322  	// Setup dcrdata websocket connection
   323  	ws, err := wsdcrdata.New(p.dcrdataHostWS())
   324  	if err != nil {
   325  		// Continue even if a websocket connection was not able to be
   326  		// made. The application specific websocket setup (pi, cms, etc)
   327  		// can decide whether to attempt reconnection or to exit.
   328  		log.Errorf("wsdcrdata New: %v", err)
   329  	}
   330  	p.wsDcrdata = ws
   331  
   332  	// Setup cmsdb
   333  	net := filepath.Base(p.cfg.DataDir)
   334  	p.cmsDB, err = cmsdb.New(p.cfg.DBHost, net, p.cfg.DBRootCert,
   335  		p.cfg.DBCert, p.cfg.DBKey)
   336  	if errors.Is(err, cmsdatabase.ErrNoVersionRecord) ||
   337  		errors.Is(err, cmsdatabase.ErrWrongVersion) {
   338  		// The cmsdb version record was either not found or
   339  		// is the wrong version which means that the cmsdb
   340  		// needs to be built/rebuilt.
   341  		p.cfg.BuildCMSDB = true
   342  	} else if err != nil {
   343  		return err
   344  	}
   345  	err = p.cmsDB.Setup()
   346  	if err != nil {
   347  		return fmt.Errorf("cmsdb setup: %v", err)
   348  	}
   349  
   350  	// Build the cms database
   351  	if p.cfg.BuildCMSDB {
   352  		index := 0
   353  		// Do pagination since we can't handle the full payload
   354  		count := 50
   355  		dbInvs := make([]database.Invoice, 0, 2048)
   356  		dbDCCs := make([]database.DCC, 0, 2048)
   357  		for {
   358  			log.Infof("requesting record inventory index %v of count %v", index, count)
   359  			// Request full record inventory from backend
   360  			challenge, err := util.Random(pd.ChallengeSize)
   361  			if err != nil {
   362  				return err
   363  			}
   364  
   365  			pdCommand := pd.Inventory{
   366  				Challenge:    hex.EncodeToString(challenge),
   367  				IncludeFiles: true,
   368  				AllVersions:  true,
   369  				VettedCount:  uint(count),
   370  				VettedStart:  uint(index),
   371  			}
   372  
   373  			ctx := context.Background()
   374  			responseBody, err := p.makeRequest(ctx, http.MethodPost,
   375  				pd.InventoryRoute, pdCommand)
   376  			if err != nil {
   377  				return err
   378  			}
   379  
   380  			var pdReply pd.InventoryReply
   381  			err = json.Unmarshal(responseBody, &pdReply)
   382  			if err != nil {
   383  				return fmt.Errorf("Could not unmarshal InventoryReply: %v",
   384  					err)
   385  			}
   386  
   387  			// Verify the UpdateVettedMetadata challenge.
   388  			err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
   389  			if err != nil {
   390  				return err
   391  			}
   392  
   393  			vetted := pdReply.Vetted
   394  			for _, r := range vetted {
   395  				for _, m := range r.Metadata {
   396  					switch m.ID {
   397  					case mdstream.IDInvoiceGeneral:
   398  						i, err := convertRecordToDatabaseInvoice(r)
   399  						if err != nil {
   400  							log.Errorf("convertRecordToDatabaseInvoice: %v", err)
   401  							break
   402  						}
   403  						u, err := p.db.UserGetByPubKey(i.PublicKey)
   404  						if err != nil {
   405  							log.Errorf("usergetbypubkey: %v %v", err, i.PublicKey)
   406  							break
   407  						}
   408  						i.UserID = u.ID.String()
   409  						i.Username = u.Username
   410  						dbInvs = append(dbInvs, *i)
   411  					case mdstream.IDDCCGeneral:
   412  						d, err := convertRecordToDatabaseDCC(r)
   413  						if err != nil {
   414  							log.Errorf("convertRecordToDatabaseDCC: %v", err)
   415  							break
   416  						}
   417  						dbDCCs = append(dbDCCs, *d)
   418  					}
   419  				}
   420  			}
   421  			if len(vetted) < count {
   422  				break
   423  			}
   424  			index += count
   425  		}
   426  
   427  		// Build the cache
   428  		err = p.cmsDB.Build(dbInvs, dbDCCs)
   429  		if err != nil {
   430  			return fmt.Errorf("build cache: %v", err)
   431  		}
   432  	}
   433  	if p.cfg.GithubAPIToken != "" {
   434  		p.tracker, err = ghtracker.New(p.cfg.GithubAPIToken,
   435  			p.cfg.DBHost, p.cfg.DBRootCert, p.cfg.DBCert, p.cfg.DBKey)
   436  		if err != nil {
   437  			return fmt.Errorf("code tracker failed to load: %v", err)
   438  		}
   439  		go func() {
   440  			err = p.updateCodeStats(p.cfg.CodeStatSkipSync,
   441  				p.cfg.CodeStatRepos, p.cfg.CodeStatStart, p.cfg.CodeStatEnd)
   442  			if err != nil {
   443  				log.Errorf("erroring updating code stats %v", err)
   444  			}
   445  		}()
   446  	}
   447  
   448  	// Register cms userdb plugin
   449  	plugin := user.Plugin{
   450  		ID:      user.CMSPluginID,
   451  		Version: user.CMSPluginVersion,
   452  	}
   453  	err = p.db.RegisterPlugin(plugin)
   454  	if err != nil {
   455  		return fmt.Errorf("register userdb plugin: %v", err)
   456  	}
   457  
   458  	// Setup invoice notifications
   459  	p.cron = cron.New()
   460  	p.checkInvoiceNotifications()
   461  
   462  	// Setup dcrdata websocket subscriptions and monitoring. This is
   463  	// done in a go routine so cmswww startup will continue in
   464  	// the event that a dcrdata websocket connection was not able to
   465  	// be made during client initialization and reconnection attempts
   466  	// are required.
   467  	go func() {
   468  		p.setupCMSAddressWatcher()
   469  	}()
   470  
   471  	return nil
   472  }
   473  
   474  // getPluginInventory returns the politeiad plugin inventory. If a politeiad
   475  // connection cannot be made, the call will be retried every 5 seconds for up
   476  // to 1000 tries.
   477  func (p *Politeiawww) getPluginInventory() ([]pdv2.Plugin, error) {
   478  	// Attempt to fetch the plugin inventory from politeiad until
   479  	// either it is successful or the maxRetries has been exceeded.
   480  	var (
   481  		done          bool
   482  		maxRetries    = 1000
   483  		sleepInterval = 5 * time.Second
   484  		plugins       = make([]pdv2.Plugin, 0, 32)
   485  		ctx           = context.Background()
   486  	)
   487  	for retries := 0; !done; retries++ {
   488  		if ctx.Err() != nil {
   489  			return nil, ctx.Err()
   490  		}
   491  		if retries == maxRetries {
   492  			return nil, fmt.Errorf("max retries exceeded")
   493  		}
   494  
   495  		pi, err := p.politeiad.PluginInventory(ctx)
   496  		if err != nil {
   497  			log.Infof("cannot get politeiad plugin inventory: %v: retry in %v",
   498  				err, sleepInterval)
   499  			time.Sleep(sleepInterval)
   500  			continue
   501  		}
   502  		plugins = append(plugins, pi...)
   503  
   504  		done = true
   505  	}
   506  
   507  	return plugins, nil
   508  }