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.
 
 
 
 
 

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