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.
 
 
 
 
 

648 lines
16 KiB

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