A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 

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