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.
 
 
 
 
 

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