A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

522 рядки
13 KiB

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