A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

735 linhas
18 KiB

  1. /*
  2. * Copyright © 2018-2019 A Bunch Tell LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "database/sql"
  13. "fmt"
  14. "html/template"
  15. "io/ioutil"
  16. "net/http"
  17. "net/url"
  18. "os"
  19. "os/signal"
  20. "path/filepath"
  21. "regexp"
  22. "strings"
  23. "syscall"
  24. "time"
  25. "github.com/gorilla/mux"
  26. "github.com/gorilla/schema"
  27. "github.com/gorilla/sessions"
  28. "github.com/manifoldco/promptui"
  29. "github.com/writeas/go-strip-markdown"
  30. "github.com/writeas/impart"
  31. "github.com/writeas/web-core/auth"
  32. "github.com/writeas/web-core/converter"
  33. "github.com/writeas/web-core/log"
  34. "github.com/writeas/writefreely/author"
  35. "github.com/writeas/writefreely/config"
  36. "github.com/writeas/writefreely/key"
  37. "github.com/writeas/writefreely/migrations"
  38. "github.com/writeas/writefreely/page"
  39. )
  40. const (
  41. staticDir = "static"
  42. assumedTitleLen = 80
  43. postsPerPage = 10
  44. serverSoftware = "WriteFreely"
  45. softwareURL = "https://writefreely.org"
  46. )
  47. var (
  48. debugging bool
  49. // Software version can be set from git env using -ldflags
  50. softwareVer = "0.9.0"
  51. // DEPRECATED VARS
  52. isSingleUser bool
  53. )
  54. // App holds data and configuration for an individual WriteFreely instance.
  55. type App struct {
  56. router *mux.Router
  57. shttp *http.ServeMux
  58. db *datastore
  59. cfg *config.Config
  60. cfgFile string
  61. keys *key.Keychain
  62. sessionStore *sessions.CookieStore
  63. formDecoder *schema.Decoder
  64. timeline *localTimeline
  65. }
  66. // DB returns the App's datastore
  67. func (app *App) DB() *datastore {
  68. return app.db
  69. }
  70. // Router returns the App's router
  71. func (app *App) Router() *mux.Router {
  72. return app.router
  73. }
  74. // Config returns the App's current configuration.
  75. func (app *App) Config() *config.Config {
  76. return app.cfg
  77. }
  78. // SetConfig updates the App's Config to the given value.
  79. func (app *App) SetConfig(cfg *config.Config) {
  80. app.cfg = cfg
  81. }
  82. // SetKeys updates the App's Keychain to the given value.
  83. func (app *App) SetKeys(k *key.Keychain) {
  84. app.keys = k
  85. }
  86. // Apper is the interface for getting data into and out of a WriteFreely
  87. // instance (or "App").
  88. //
  89. // App returns the App for the current instance.
  90. //
  91. // LoadConfig reads an app configuration into the App, returning any error
  92. // encountered.
  93. //
  94. // SaveConfig persists the current App configuration.
  95. //
  96. // LoadKeys reads the App's encryption keys and loads them into its
  97. // key.Keychain.
  98. type Apper interface {
  99. App() *App
  100. LoadConfig() error
  101. SaveConfig(*config.Config) error
  102. LoadKeys() error
  103. }
  104. // App returns the App
  105. func (app *App) App() *App {
  106. return app
  107. }
  108. // LoadConfig loads and parses a config file.
  109. func (app *App) LoadConfig() error {
  110. log.Info("Loading %s configuration...", app.cfgFile)
  111. cfg, err := config.Load(app.cfgFile)
  112. if err != nil {
  113. log.Error("Unable to load configuration: %v", err)
  114. os.Exit(1)
  115. return err
  116. }
  117. app.cfg = cfg
  118. return nil
  119. }
  120. // SaveConfig saves the given Config to disk -- namely, to the App's cfgFile.
  121. func (app *App) SaveConfig(c *config.Config) error {
  122. return config.Save(c, app.cfgFile)
  123. }
  124. // LoadKeys reads all needed keys from disk into the App. In order to use the
  125. // configured `Server.KeysParentDir`, you must call initKeyPaths(App) before
  126. // this.
  127. func (app *App) LoadKeys() error {
  128. var err error
  129. app.keys = &key.Keychain{}
  130. if debugging {
  131. log.Info(" %s", emailKeyPath)
  132. }
  133. app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath)
  134. if err != nil {
  135. return err
  136. }
  137. if debugging {
  138. log.Info(" %s", cookieAuthKeyPath)
  139. }
  140. app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
  141. if err != nil {
  142. return err
  143. }
  144. if debugging {
  145. log.Info(" %s", cookieKeyPath)
  146. }
  147. app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath)
  148. if err != nil {
  149. return err
  150. }
  151. return nil
  152. }
  153. // handleViewHome shows page at root path. Will be the Pad if logged in and the
  154. // catch-all landing page otherwise.
  155. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
  156. if app.cfg.App.SingleUser {
  157. // Render blog index
  158. return handleViewCollection(app, w, r)
  159. }
  160. // Multi-user instance
  161. u := getUserSession(app, r)
  162. if u != nil {
  163. // User is logged in, so show the Pad
  164. return handleViewPad(app, w, r)
  165. }
  166. if land := app.cfg.App.LandingPath(); land != "/" {
  167. return impart.HTTPError{http.StatusFound, land}
  168. }
  169. p := struct {
  170. page.StaticPage
  171. Flashes []template.HTML
  172. }{
  173. StaticPage: pageForReq(app, r),
  174. }
  175. // Get error messages
  176. session, err := app.sessionStore.Get(r, cookieName)
  177. if err != nil {
  178. // Ignore this
  179. log.Error("Unable to get session in handleViewHome; ignoring: %v", err)
  180. }
  181. flashes, _ := getSessionFlashes(app, w, r, session)
  182. for _, flash := range flashes {
  183. p.Flashes = append(p.Flashes, template.HTML(flash))
  184. }
  185. // Show landing page
  186. return renderPage(w, "landing.tmpl", p)
  187. }
  188. func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *template.Template) error {
  189. p := struct {
  190. page.StaticPage
  191. ContentTitle string
  192. Content template.HTML
  193. PlainContent string
  194. Updated string
  195. AboutStats *InstanceStats
  196. }{
  197. StaticPage: pageForReq(app, r),
  198. }
  199. if r.URL.Path == "/about" || r.URL.Path == "/privacy" {
  200. var c *instanceContent
  201. var err error
  202. if r.URL.Path == "/about" {
  203. c, err = getAboutPage(app)
  204. // Fetch stats
  205. p.AboutStats = &InstanceStats{}
  206. p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
  207. p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
  208. } else {
  209. c, err = getPrivacyPage(app)
  210. }
  211. if err != nil {
  212. return err
  213. }
  214. p.ContentTitle = c.Title.String
  215. p.Content = template.HTML(applyMarkdown([]byte(c.Content), ""))
  216. p.PlainContent = shortPostDescription(stripmd.Strip(c.Content))
  217. if !c.Updated.IsZero() {
  218. p.Updated = c.Updated.Format("January 2, 2006")
  219. }
  220. }
  221. // Serve templated page
  222. err := t.ExecuteTemplate(w, "base", p)
  223. if err != nil {
  224. log.Error("Unable to render page: %v", err)
  225. }
  226. return nil
  227. }
  228. func pageForReq(app *App, r *http.Request) page.StaticPage {
  229. p := page.StaticPage{
  230. AppCfg: app.cfg.App,
  231. Path: r.URL.Path,
  232. Version: "v" + softwareVer,
  233. }
  234. // Add user information, if given
  235. var u *User
  236. accessToken := r.FormValue("t")
  237. if accessToken != "" {
  238. userID := app.db.GetUserID(accessToken)
  239. if userID != -1 {
  240. var err error
  241. u, err = app.db.GetUserByID(userID)
  242. if err == nil {
  243. p.Username = u.Username
  244. }
  245. }
  246. } else {
  247. u = getUserSession(app, r)
  248. if u != nil {
  249. p.Username = u.Username
  250. }
  251. }
  252. return p
  253. }
  254. var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
  255. // Initialize loads the app configuration and initializes templates, keys,
  256. // session, route handlers, and the database connection.
  257. func Initialize(apper Apper, debug bool) (*App, error) {
  258. debugging = debug
  259. apper.LoadConfig()
  260. // Load templates
  261. err := InitTemplates(apper.App().Config())
  262. if err != nil {
  263. return nil, fmt.Errorf("load templates: %s", err)
  264. }
  265. // Load keys and set up session
  266. initKeyPaths(apper.App()) // TODO: find a better way to do this, since it's unneeded in all Apper implementations
  267. err = InitKeys(apper)
  268. if err != nil {
  269. return nil, fmt.Errorf("init keys: %s", err)
  270. }
  271. apper.App().InitSession()
  272. apper.App().InitDecoder()
  273. err = ConnectToDatabase(apper.App())
  274. if err != nil {
  275. return nil, fmt.Errorf("connect to DB: %s", err)
  276. }
  277. // Handle local timeline, if enabled
  278. if apper.App().cfg.App.LocalTimeline {
  279. log.Info("Initializing local timeline...")
  280. initLocalTimeline(apper.App())
  281. }
  282. return apper.App(), nil
  283. }
  284. func Serve(app *App, r *mux.Router) {
  285. log.Info("Going to serve...")
  286. isSingleUser = app.cfg.App.SingleUser
  287. app.cfg.Server.Dev = debugging
  288. // Handle shutdown
  289. c := make(chan os.Signal, 2)
  290. signal.Notify(c, os.Interrupt, syscall.SIGTERM)
  291. go func() {
  292. <-c
  293. log.Info("Shutting down...")
  294. shutdown(app)
  295. log.Info("Done.")
  296. os.Exit(0)
  297. }()
  298. // Start web application server
  299. var bindAddress = app.cfg.Server.Bind
  300. if bindAddress == "" {
  301. bindAddress = "localhost"
  302. }
  303. var err error
  304. if app.cfg.IsSecureStandalone() {
  305. log.Info("Serving redirects on http://%s:80", bindAddress)
  306. go func() {
  307. err = http.ListenAndServe(
  308. fmt.Sprintf("%s:80", bindAddress), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  309. http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently)
  310. }))
  311. log.Error("Unable to start redirect server: %v", err)
  312. }()
  313. log.Info("Serving on https://%s:443", bindAddress)
  314. log.Info("---")
  315. err = http.ListenAndServeTLS(
  316. fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r)
  317. } else {
  318. log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
  319. log.Info("---")
  320. err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r)
  321. }
  322. if err != nil {
  323. log.Error("Unable to start: %v", err)
  324. os.Exit(1)
  325. }
  326. }
  327. func (app *App) InitDecoder() {
  328. // TODO: do this at the package level, instead of the App level
  329. // Initialize modules
  330. app.formDecoder = schema.NewDecoder()
  331. app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
  332. app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
  333. app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
  334. app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
  335. app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
  336. app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
  337. }
  338. // ConnectToDatabase validates and connects to the configured database, then
  339. // tests the connection.
  340. func ConnectToDatabase(app *App) error {
  341. // Check database configuration
  342. if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
  343. return fmt.Errorf("Database user or password not set.")
  344. }
  345. if app.cfg.Database.Host == "" {
  346. app.cfg.Database.Host = "localhost"
  347. }
  348. if app.cfg.Database.Database == "" {
  349. app.cfg.Database.Database = "writefreely"
  350. }
  351. // TODO: check err
  352. connectToDatabase(app)
  353. // Test database connection
  354. err := app.db.Ping()
  355. if err != nil {
  356. return fmt.Errorf("Database ping failed: %s", err)
  357. }
  358. return nil
  359. }
  360. // OutputVersion prints out the version of the application.
  361. func OutputVersion() {
  362. fmt.Println(serverSoftware + " " + softwareVer)
  363. }
  364. // NewApp creates a new app instance.
  365. func NewApp(cfgFile string) *App {
  366. return &App{
  367. cfgFile: cfgFile,
  368. }
  369. }
  370. // CreateConfig creates a default configuration and saves it to the app's cfgFile.
  371. func CreateConfig(app *App) error {
  372. log.Info("Creating configuration...")
  373. c := config.New()
  374. log.Info("Saving configuration %s...", app.cfgFile)
  375. err := config.Save(c, app.cfgFile)
  376. if err != nil {
  377. return fmt.Errorf("Unable to save configuration: %v", err)
  378. }
  379. return nil
  380. }
  381. // DoConfig runs the interactive configuration process.
  382. func DoConfig(app *App, configSections string) {
  383. if configSections == "" {
  384. configSections = "server db app"
  385. }
  386. // let's check there aren't any garbage in the list
  387. configSectionsArray := strings.Split(configSections, " ")
  388. for _, element := range configSectionsArray {
  389. if element != "server" && element != "db" && element != "app" {
  390. log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
  391. os.Exit(1)
  392. }
  393. }
  394. d, err := config.Configure(app.cfgFile, configSections)
  395. if err != nil {
  396. log.Error("Unable to configure: %v", err)
  397. os.Exit(1)
  398. }
  399. if d.User != nil {
  400. app.cfg = d.Config
  401. connectToDatabase(app)
  402. defer shutdown(app)
  403. if !app.db.DatabaseInitialized() {
  404. err = adminInitDatabase(app)
  405. if err != nil {
  406. log.Error(err.Error())
  407. os.Exit(1)
  408. }
  409. }
  410. u := &User{
  411. Username: d.User.Username,
  412. HashedPass: d.User.HashedPass,
  413. Created: time.Now().Truncate(time.Second).UTC(),
  414. }
  415. // Create blog
  416. log.Info("Creating user %s...\n", u.Username)
  417. err = app.db.CreateUser(u, app.cfg.App.SiteName)
  418. if err != nil {
  419. log.Error("Unable to create user: %s", err)
  420. os.Exit(1)
  421. }
  422. log.Info("Done!")
  423. }
  424. os.Exit(0)
  425. }
  426. // GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir.
  427. func GenerateKeyFiles(app *App) error {
  428. // Read keys path from config
  429. app.LoadConfig()
  430. // Create keys dir if it doesn't exist yet
  431. fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
  432. if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
  433. err = os.Mkdir(fullKeysDir, 0700)
  434. if err != nil {
  435. return err
  436. }
  437. }
  438. // Generate keys
  439. initKeyPaths(app)
  440. // TODO: use something like https://github.com/hashicorp/go-multierror to return errors
  441. var keyErrs error
  442. err := generateKey(emailKeyPath)
  443. if err != nil {
  444. keyErrs = err
  445. }
  446. err = generateKey(cookieAuthKeyPath)
  447. if err != nil {
  448. keyErrs = err
  449. }
  450. err = generateKey(cookieKeyPath)
  451. if err != nil {
  452. keyErrs = err
  453. }
  454. return keyErrs
  455. }
  456. // CreateSchema creates all database tables needed for the application.
  457. func CreateSchema(apper Apper) error {
  458. apper.LoadConfig()
  459. connectToDatabase(apper.App())
  460. defer shutdown(apper.App())
  461. err := adminInitDatabase(apper.App())
  462. if err != nil {
  463. return err
  464. }
  465. return nil
  466. }
  467. // Migrate runs all necessary database migrations.
  468. func Migrate(app *App) error {
  469. app.LoadConfig()
  470. connectToDatabase(app)
  471. defer shutdown(app)
  472. err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
  473. if err != nil {
  474. return fmt.Errorf("migrate: %s", err)
  475. }
  476. return nil
  477. }
  478. // ResetPassword runs the interactive password reset process.
  479. func ResetPassword(app *App, username string) error {
  480. // Connect to the database
  481. app.LoadConfig()
  482. connectToDatabase(app)
  483. defer shutdown(app)
  484. // Fetch user
  485. u, err := app.db.GetUserForAuth(username)
  486. if err != nil {
  487. log.Error("Get user: %s", err)
  488. os.Exit(1)
  489. }
  490. // Prompt for new password
  491. prompt := promptui.Prompt{
  492. Templates: &promptui.PromptTemplates{
  493. Success: "{{ . | bold | faint }}: ",
  494. },
  495. Label: "New password",
  496. Mask: '*',
  497. }
  498. newPass, err := prompt.Run()
  499. if err != nil {
  500. log.Error("%s", err)
  501. os.Exit(1)
  502. }
  503. // Do the update
  504. log.Info("Updating...")
  505. err = adminResetPassword(app, u, newPass)
  506. if err != nil {
  507. log.Error("%s", err)
  508. os.Exit(1)
  509. }
  510. log.Info("Success.")
  511. return nil
  512. }
  513. func connectToDatabase(app *App) {
  514. log.Info("Connecting to %s database...", app.cfg.Database.Type)
  515. var db *sql.DB
  516. var err error
  517. if app.cfg.Database.Type == driverMySQL {
  518. db, err = sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
  519. db.SetMaxOpenConns(50)
  520. } else if app.cfg.Database.Type == driverSQLite {
  521. if !SQLiteEnabled {
  522. log.Error("Invalid database type '%s'. Binary wasn't compiled with SQLite3 support.", app.cfg.Database.Type)
  523. os.Exit(1)
  524. }
  525. if app.cfg.Database.FileName == "" {
  526. log.Error("SQLite database filename value in config.ini is empty.")
  527. os.Exit(1)
  528. }
  529. db, err = sql.Open("sqlite3_with_regex", app.cfg.Database.FileName+"?parseTime=true&cached=shared")
  530. db.SetMaxOpenConns(1)
  531. } else {
  532. log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type)
  533. os.Exit(1)
  534. }
  535. if err != nil {
  536. log.Error("%s", err)
  537. os.Exit(1)
  538. }
  539. app.db = &datastore{db, app.cfg.Database.Type}
  540. }
  541. func shutdown(app *App) {
  542. log.Info("Closing database connection...")
  543. app.db.Close()
  544. }
  545. // CreateUser creates a new admin or normal user from the given credentials.
  546. func CreateUser(apper Apper, username, password string, isAdmin bool) error {
  547. // Create an admin user with --create-admin
  548. apper.LoadConfig()
  549. connectToDatabase(apper.App())
  550. defer shutdown(apper.App())
  551. // Ensure an admin / first user doesn't already exist
  552. firstUser, _ := apper.App().db.GetUserByID(1)
  553. if isAdmin {
  554. // Abort if trying to create admin user, but one already exists
  555. if firstUser != nil {
  556. return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely --create-user", firstUser.Username)
  557. }
  558. } else {
  559. // Abort if trying to create regular user, but no admin exists yet
  560. if firstUser == nil {
  561. return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely --create-admin")
  562. }
  563. }
  564. // Create the user
  565. // Normalize and validate username
  566. desiredUsername := username
  567. username = getSlug(username, "")
  568. usernameDesc := username
  569. if username != desiredUsername {
  570. usernameDesc += " (originally: " + desiredUsername + ")"
  571. }
  572. if !author.IsValidUsername(apper.App().cfg, username) {
  573. return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen)
  574. }
  575. // Hash the password
  576. hashedPass, err := auth.HashPass([]byte(password))
  577. if err != nil {
  578. return fmt.Errorf("Unable to hash password: %v", err)
  579. }
  580. u := &User{
  581. Username: username,
  582. HashedPass: hashedPass,
  583. Created: time.Now().Truncate(time.Second).UTC(),
  584. }
  585. userType := "user"
  586. if isAdmin {
  587. userType = "admin"
  588. }
  589. log.Info("Creating %s %s...", userType, usernameDesc)
  590. err = apper.App().db.CreateUser(u, desiredUsername)
  591. if err != nil {
  592. return fmt.Errorf("Unable to create user: %s", err)
  593. }
  594. log.Info("Done!")
  595. return nil
  596. }
  597. func adminInitDatabase(app *App) error {
  598. schemaFileName := "schema.sql"
  599. if app.cfg.Database.Type == driverSQLite {
  600. schemaFileName = "sqlite.sql"
  601. }
  602. schema, err := Asset(schemaFileName)
  603. if err != nil {
  604. return fmt.Errorf("Unable to load schema file: %v", err)
  605. }
  606. tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
  607. queries := strings.Split(string(schema), ";\n")
  608. for _, q := range queries {
  609. if strings.TrimSpace(q) == "" {
  610. continue
  611. }
  612. parts := tblReg.FindStringSubmatch(q)
  613. if len(parts) >= 3 {
  614. log.Info("Creating table %s...", parts[2])
  615. } else {
  616. log.Info("Creating table ??? (Weird query) No match in: %v", parts)
  617. }
  618. _, err = app.db.Exec(q)
  619. if err != nil {
  620. log.Error("%s", err)
  621. } else {
  622. log.Info("Created.")
  623. }
  624. }
  625. // Set up migrations table
  626. log.Info("Initializing appmigrations table...")
  627. err = migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName))
  628. if err != nil {
  629. return fmt.Errorf("Unable to set initial migrations: %v", err)
  630. }
  631. log.Info("Running migrations...")
  632. err = migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
  633. if err != nil {
  634. return fmt.Errorf("migrate: %s", err)
  635. }
  636. log.Info("Done.")
  637. return nil
  638. }