A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

724 lines
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) {
  383. d, err := config.Configure(app.cfgFile)
  384. if err != nil {
  385. log.Error("Unable to configure: %v", err)
  386. os.Exit(1)
  387. }
  388. if d.User != nil {
  389. app.cfg = d.Config
  390. connectToDatabase(app)
  391. defer shutdown(app)
  392. if !app.db.DatabaseInitialized() {
  393. err = adminInitDatabase(app)
  394. if err != nil {
  395. log.Error(err.Error())
  396. os.Exit(1)
  397. }
  398. }
  399. u := &User{
  400. Username: d.User.Username,
  401. HashedPass: d.User.HashedPass,
  402. Created: time.Now().Truncate(time.Second).UTC(),
  403. }
  404. // Create blog
  405. log.Info("Creating user %s...\n", u.Username)
  406. err = app.db.CreateUser(u, app.cfg.App.SiteName)
  407. if err != nil {
  408. log.Error("Unable to create user: %s", err)
  409. os.Exit(1)
  410. }
  411. log.Info("Done!")
  412. }
  413. os.Exit(0)
  414. }
  415. // GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir.
  416. func GenerateKeyFiles(app *App) error {
  417. // Read keys path from config
  418. app.LoadConfig()
  419. // Create keys dir if it doesn't exist yet
  420. fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
  421. if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
  422. err = os.Mkdir(fullKeysDir, 0700)
  423. if err != nil {
  424. return err
  425. }
  426. }
  427. // Generate keys
  428. initKeyPaths(app)
  429. // TODO: use something like https://github.com/hashicorp/go-multierror to return errors
  430. var keyErrs error
  431. err := generateKey(emailKeyPath)
  432. if err != nil {
  433. keyErrs = err
  434. }
  435. err = generateKey(cookieAuthKeyPath)
  436. if err != nil {
  437. keyErrs = err
  438. }
  439. err = generateKey(cookieKeyPath)
  440. if err != nil {
  441. keyErrs = err
  442. }
  443. return keyErrs
  444. }
  445. // CreateSchema creates all database tables needed for the application.
  446. func CreateSchema(apper Apper) error {
  447. apper.LoadConfig()
  448. connectToDatabase(apper.App())
  449. defer shutdown(apper.App())
  450. err := adminInitDatabase(apper.App())
  451. if err != nil {
  452. return err
  453. }
  454. return nil
  455. }
  456. // Migrate runs all necessary database migrations.
  457. func Migrate(app *App) error {
  458. app.LoadConfig()
  459. connectToDatabase(app)
  460. defer shutdown(app)
  461. err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
  462. if err != nil {
  463. return fmt.Errorf("migrate: %s", err)
  464. }
  465. return nil
  466. }
  467. // ResetPassword runs the interactive password reset process.
  468. func ResetPassword(app *App, username string) error {
  469. // Connect to the database
  470. app.LoadConfig()
  471. connectToDatabase(app)
  472. defer shutdown(app)
  473. // Fetch user
  474. u, err := app.db.GetUserForAuth(username)
  475. if err != nil {
  476. log.Error("Get user: %s", err)
  477. os.Exit(1)
  478. }
  479. // Prompt for new password
  480. prompt := promptui.Prompt{
  481. Templates: &promptui.PromptTemplates{
  482. Success: "{{ . | bold | faint }}: ",
  483. },
  484. Label: "New password",
  485. Mask: '*',
  486. }
  487. newPass, err := prompt.Run()
  488. if err != nil {
  489. log.Error("%s", err)
  490. os.Exit(1)
  491. }
  492. // Do the update
  493. log.Info("Updating...")
  494. err = adminResetPassword(app, u, newPass)
  495. if err != nil {
  496. log.Error("%s", err)
  497. os.Exit(1)
  498. }
  499. log.Info("Success.")
  500. return nil
  501. }
  502. func connectToDatabase(app *App) {
  503. log.Info("Connecting to %s database...", app.cfg.Database.Type)
  504. var db *sql.DB
  505. var err error
  506. if app.cfg.Database.Type == driverMySQL {
  507. 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())))
  508. db.SetMaxOpenConns(50)
  509. } else if app.cfg.Database.Type == driverSQLite {
  510. if !SQLiteEnabled {
  511. log.Error("Invalid database type '%s'. Binary wasn't compiled with SQLite3 support.", app.cfg.Database.Type)
  512. os.Exit(1)
  513. }
  514. if app.cfg.Database.FileName == "" {
  515. log.Error("SQLite database filename value in config.ini is empty.")
  516. os.Exit(1)
  517. }
  518. db, err = sql.Open("sqlite3_with_regex", app.cfg.Database.FileName+"?parseTime=true&cached=shared")
  519. db.SetMaxOpenConns(1)
  520. } else {
  521. log.Error("Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now.", app.cfg.Database.Type)
  522. os.Exit(1)
  523. }
  524. if err != nil {
  525. log.Error("%s", err)
  526. os.Exit(1)
  527. }
  528. app.db = &datastore{db, app.cfg.Database.Type}
  529. }
  530. func shutdown(app *App) {
  531. log.Info("Closing database connection...")
  532. app.db.Close()
  533. }
  534. // CreateUser creates a new admin or normal user from the given credentials.
  535. func CreateUser(apper Apper, username, password string, isAdmin bool) error {
  536. // Create an admin user with --create-admin
  537. apper.LoadConfig()
  538. connectToDatabase(apper.App())
  539. defer shutdown(apper.App())
  540. // Ensure an admin / first user doesn't already exist
  541. firstUser, _ := apper.App().db.GetUserByID(1)
  542. if isAdmin {
  543. // Abort if trying to create admin user, but one already exists
  544. if firstUser != nil {
  545. return fmt.Errorf("Admin user already exists (%s). Create a regular user with: writefreely --create-user", firstUser.Username)
  546. }
  547. } else {
  548. // Abort if trying to create regular user, but no admin exists yet
  549. if firstUser == nil {
  550. return fmt.Errorf("No admin user exists yet. Create an admin first with: writefreely --create-admin")
  551. }
  552. }
  553. // Create the user
  554. // Normalize and validate username
  555. desiredUsername := username
  556. username = getSlug(username, "")
  557. usernameDesc := username
  558. if username != desiredUsername {
  559. usernameDesc += " (originally: " + desiredUsername + ")"
  560. }
  561. if !author.IsValidUsername(apper.App().cfg, username) {
  562. return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen)
  563. }
  564. // Hash the password
  565. hashedPass, err := auth.HashPass([]byte(password))
  566. if err != nil {
  567. return fmt.Errorf("Unable to hash password: %v", err)
  568. }
  569. u := &User{
  570. Username: username,
  571. HashedPass: hashedPass,
  572. Created: time.Now().Truncate(time.Second).UTC(),
  573. }
  574. userType := "user"
  575. if isAdmin {
  576. userType = "admin"
  577. }
  578. log.Info("Creating %s %s...", userType, usernameDesc)
  579. err = apper.App().db.CreateUser(u, desiredUsername)
  580. if err != nil {
  581. return fmt.Errorf("Unable to create user: %s", err)
  582. }
  583. log.Info("Done!")
  584. return nil
  585. }
  586. func adminInitDatabase(app *App) error {
  587. schemaFileName := "schema.sql"
  588. if app.cfg.Database.Type == driverSQLite {
  589. schemaFileName = "sqlite.sql"
  590. }
  591. schema, err := Asset(schemaFileName)
  592. if err != nil {
  593. return fmt.Errorf("Unable to load schema file: %v", err)
  594. }
  595. tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
  596. queries := strings.Split(string(schema), ";\n")
  597. for _, q := range queries {
  598. if strings.TrimSpace(q) == "" {
  599. continue
  600. }
  601. parts := tblReg.FindStringSubmatch(q)
  602. if len(parts) >= 3 {
  603. log.Info("Creating table %s...", parts[2])
  604. } else {
  605. log.Info("Creating table ??? (Weird query) No match in: %v", parts)
  606. }
  607. _, err = app.db.Exec(q)
  608. if err != nil {
  609. log.Error("%s", err)
  610. } else {
  611. log.Info("Created.")
  612. }
  613. }
  614. // Set up migrations table
  615. log.Info("Initializing appmigrations table...")
  616. err = migrations.SetInitialMigrations(migrations.NewDatastore(app.db.DB, app.db.driverName))
  617. if err != nil {
  618. return fmt.Errorf("Unable to set initial migrations: %v", err)
  619. }
  620. log.Info("Running migrations...")
  621. err = migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
  622. if err != nil {
  623. return fmt.Errorf("migrate: %s", err)
  624. }
  625. log.Info("Done.")
  626. return nil
  627. }