A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 

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