config.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. //
  2. // Copyright (C) 2017-2018 Marcus Rohrmoser, http://purl.mro.name/ShaarliGo
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. //
  17. package main
  18. import (
  19. "html/template"
  20. "io"
  21. "log"
  22. "net/http"
  23. "net/url"
  24. "os"
  25. "path"
  26. "strings"
  27. "time"
  28. "golang.org/x/crypto/bcrypt"
  29. )
  30. func xmlBaseFromRequestURL(r *url.URL, scriptName string) *url.URL {
  31. dir := path.Dir(scriptName)
  32. if dir[len(dir)-1:] != "/" {
  33. dir = dir + "/"
  34. }
  35. return mustParseURL(r.Scheme + "://" + r.Host + dir)
  36. }
  37. func mustParseRFC3339(str string) time.Time {
  38. if ret, err := time.Parse(time.RFC3339, str); err != nil {
  39. panic(err)
  40. } else {
  41. return ret
  42. }
  43. }
  44. func (app *App) handleSettings(w http.ResponseWriter, r *http.Request) {
  45. now := time.Now()
  46. if app.cfg.IsConfigured() && !app.IsLoggedIn(now) {
  47. http.Error(w, "double check failed.", http.StatusInternalServerError)
  48. return
  49. }
  50. if err := r.ParseForm(); err != nil {
  51. http.Error(w, "couldn't parse form: "+err.Error(), http.StatusInternalServerError)
  52. return
  53. }
  54. switch r.Method {
  55. case http.MethodPost:
  56. uid := strings.TrimSpace(r.FormValue("setlogin"))
  57. pwd := strings.TrimSpace(r.FormValue("setpassword"))
  58. title := strings.TrimSpace(r.FormValue("title"))
  59. // https://astaxie.gitbooks.io/build-web-application-with-golang/en/09.5.html
  60. // $GLOBALS['salt'] = sha1(uniqid('',true).'_'.mt_rand()); // Salt renders rainbow-tables attacks useless.
  61. // original shaarli did $hash = sha1($password.$login.$GLOBALS['salt']);
  62. if pwdBcrypt, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost); err != nil {
  63. http.Error(w, "couldn't crypt pwd: "+err.Error(), http.StatusInternalServerError)
  64. return
  65. } else {
  66. if len(uid) < 1 || len([]rune(pwd)) < 12 {
  67. app.cfg.renderSettingsPage(w, http.StatusBadRequest)
  68. return
  69. }
  70. app.cfg.Title = title
  71. app.cfg.Uid = uid
  72. app.cfg.PwdBcrypt = string(pwdBcrypt)
  73. err = app.cfg.Save()
  74. }
  75. if feed, err := app.LoadFeed(); err != nil {
  76. http.Error(w, "couldn't load seed feed feeds: "+err.Error(), http.StatusInternalServerError)
  77. return
  78. } else {
  79. feed.XmlBase = xmlBaseFromRequestURL(r.URL, os.Getenv("SCRIPT_NAME")).String()
  80. feed.Id = feed.XmlBase // expand XmlBase as required by https://validator.w3.org/feed/check.cgi?url=
  81. feed.Title = HumanText{Body: title}
  82. feed.Authors = []Person{Person{Name: uid}}
  83. feed.Links = []Link{
  84. Link{Rel: relEdit, Href: path.Join(cgiName, uriPub, uriPosts), Title: "PostURI, maybe better a app:collection https://tools.ietf.org/html/rfc5023#section-8.3.3"},
  85. }
  86. if err := app.SaveFeed(feed); err != nil {
  87. http.Error(w, "couldn't store feed data: "+err.Error(), http.StatusInternalServerError)
  88. return
  89. }
  90. feed.XmlBase = xmlBaseFromRequestURL(r.URL, os.Getenv("SCRIPT_NAME")).String()
  91. if err := app.PublishFeedsForModifiedEntries(feed, feed.Entries); err != nil {
  92. log.Println("couldn't write feeds: ", err.Error())
  93. http.Error(w, "couldn't write feeds: "+err.Error(), http.StatusInternalServerError)
  94. return
  95. }
  96. app.startSession(w, r, now)
  97. http.Redirect(w, r, path.Join("..", "..", uriPub, uriPosts)+"/", http.StatusFound)
  98. }
  99. case http.MethodGet:
  100. app.cfg.renderSettingsPage(w, http.StatusOK)
  101. default:
  102. http.Error(w, "MethodNotAllowed", http.StatusMethodNotAllowed)
  103. }
  104. }
  105. func (cfg Config) renderSettingsPage(w http.ResponseWriter, code int) {
  106. tmpl, err := template.New("settings").Parse(`<html xmlns="http://www.w3.org/1999/xhtml">
  107. <head/>
  108. <body>
  109. <form method="post" name="installform" id="installform">
  110. <input type="text" name="setlogin" value="{{index . "setlogin"}}"/>
  111. <input type="password" name="setpassword" />
  112. <input type="text" name="title" value="{{index . "title"}}"/>
  113. <input type="submit" name="Save" value="Save config" />
  114. </form>
  115. </body>
  116. </html>
  117. `)
  118. if err == nil {
  119. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  120. w.WriteHeader(code)
  121. io.WriteString(w, "<?xml version='1.0' encoding='UTF-8'?>\n"+
  122. "<?xml-stylesheet type='text/xsl' href='"+path.Join("..", "..", "assets", cfg.Skin, "config.xslt")+"'?>\n")
  123. io.WriteString(w, `<!--
  124. The html you see here is for compatibilty with vanilla shaarli.
  125. The main reason is backward compatibility for e.g. http://app.mro.name/ShaarliOS and
  126. https://github.com/dimtion/Shaarlier as tested via
  127. https://code.mro.name/mro/Shaarli-API-test
  128. -->
  129. `)
  130. err = tmpl.Execute(w, map[string]string{
  131. "title": cfg.Title,
  132. "setlogin": cfg.Uid,
  133. })
  134. }
  135. if err != nil {
  136. http.Error(w, "couldn't restore assets: "+err.Error(), http.StatusInternalServerError)
  137. }
  138. }