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 }