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.
 
 
 
 
 

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