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.
 
 
 
 
 

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