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.
 
 
 
 
 

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