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.
 
 
 
 
 

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