A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 

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