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.
 
 
 
 
 

432 lines
10 KiB

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