Merge branch 'develop' into shorter-config-process

Move flag parsing to main.go as per the issue description
This commit is contained in:
Michael Demetriou 2019-06-21 12:07:01 +03:00
commit 6506709fbc
33 changed files with 1106 additions and 583 deletions

View file

@ -100,13 +100,16 @@ ui : force_look
cd less/; $(MAKE) $(MFLAGS) cd less/; $(MAKE) $(MFLAGS)
assets : generate assets : generate
go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
assets-no-sqlite: generate assets-no-sqlite: generate
go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql
dev-assets : generate dev-assets : generate
go-bindata -pkg writefreely -ignore=\\.gitignore -debug schema.sql sqlite.sql go-bindata -pkg writefreely -ignore=\\.gitignore -debug -tags="!wflib" schema.sql sqlite.sql
lib-assets : generate
go-bindata -pkg writefreely -ignore=\\.gitignore -o bindata-lib.go -tags="wflib" schema.sql
generate : generate :
@hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash go-bindata > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
@ -123,7 +126,7 @@ $(TMPBIN)/xgo: deps $(TMPBIN)
$(GOBUILD) -o $(TMPBIN)/xgo github.com/karalabe/xgo $(GOBUILD) -o $(TMPBIN)/xgo github.com/karalabe/xgo
ci-assets : $(TMPBIN)/go-bindata ci-assets : $(TMPBIN)/go-bindata
$(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore schema.sql sqlite.sql $(TMPBIN)/go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
clean : clean :
-rm -rf build -rm -rf build

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -49,7 +49,7 @@ type (
} }
) )
func NewUserPage(app *app, r *http.Request, u *User, title string, flashes []string) *UserPage { func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage {
up := &UserPage{ up := &UserPage{
StaticPage: pageForReq(app, r), StaticPage: pageForReq(app, r),
PageTitle: title, PageTitle: title,
@ -73,12 +73,12 @@ const (
var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead") var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead")
func apiSignup(app *app, w http.ResponseWriter, r *http.Request) error { func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
_, err := signup(app, w, r) _, err := signup(app, w, r)
return err return err
} }
func signup(app *app, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
// Get params // Get params
@ -113,7 +113,7 @@ func signup(app *app, w http.ResponseWriter, r *http.Request) (*AuthUser, error)
return signupWithRegistration(app, ur, w, r) return signupWithRegistration(app, ur, w, r)
} }
func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) { func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
// Validate required params (alias) // Validate required params (alias)
@ -154,7 +154,7 @@ func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWr
Created: time.Now().Truncate(time.Second).UTC(), Created: time.Now().Truncate(time.Second).UTC(),
} }
if signup.Email != "" { if signup.Email != "" {
encEmail, err := data.Encrypt(app.keys.emailKey, signup.Email) encEmail, err := data.Encrypt(app.keys.EmailKey, signup.Email)
if err != nil { if err != nil {
log.Error("Unable to encrypt email: %s\n", err) log.Error("Unable to encrypt email: %s\n", err)
} else { } else {
@ -229,7 +229,7 @@ func signupWithRegistration(app *app, signup userRegistration, w http.ResponseWr
return resUser, nil return resUser, nil
} }
func viewLogout(app *app, w http.ResponseWriter, r *http.Request) error { func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error {
session, err := app.sessionStore.Get(r, cookieName) session, err := app.sessionStore.Get(r, cookieName)
if err != nil { if err != nil {
return ErrInternalCookieSession return ErrInternalCookieSession
@ -268,7 +268,7 @@ func viewLogout(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.HTTPError{http.StatusFound, "/"} return impart.HTTPError{http.StatusFound, "/"}
} }
func handleAPILogout(app *app, w http.ResponseWriter, r *http.Request) error { func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error {
accessToken := r.Header.Get("Authorization") accessToken := r.Header.Get("Authorization")
if accessToken == "" { if accessToken == "" {
return ErrNoAccessToken return ErrNoAccessToken
@ -284,7 +284,7 @@ func handleAPILogout(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.HTTPError{Status: http.StatusNoContent} return impart.HTTPError{Status: http.StatusNoContent}
} }
func viewLogin(app *app, w http.ResponseWriter, r *http.Request) error { func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
var earlyError string var earlyError string
oneTimeToken := r.FormValue("with") oneTimeToken := r.FormValue("with")
if oneTimeToken != "" { if oneTimeToken != "" {
@ -333,7 +333,7 @@ func viewLogin(app *app, w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func webLogin(app *app, w http.ResponseWriter, r *http.Request) error { func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
err := login(app, w, r) err := login(app, w, r)
if err != nil { if err != nil {
username := r.FormValue("alias") username := r.FormValue("alias")
@ -370,7 +370,7 @@ func webLogin(app *app, w http.ResponseWriter, r *http.Request) error {
var loginAttemptUsers = sync.Map{} var loginAttemptUsers = sync.Map{}
func login(app *app, w http.ResponseWriter, r *http.Request) error { func login(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
oneTimeToken := r.FormValue("with") oneTimeToken := r.FormValue("with")
verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "") verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
@ -534,7 +534,7 @@ func login(app *app, w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser { func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser {
resUser := &AuthUser{ resUser := &AuthUser{
AccessToken: token, AccessToken: token,
User: u, User: u,
@ -563,7 +563,7 @@ func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser
return resUser return resUser
} }
func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
// Fetch extra user data // Fetch extra user data
p := NewUserPage(app, r, u, "Export", nil) p := NewUserPage(app, r, u, "Export", nil)
@ -571,7 +571,7 @@ func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request
return nil return nil
} }
func viewExportPosts(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
var filename string var filename string
var u = &User{} var u = &User{}
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
@ -635,7 +635,7 @@ func viewExportPosts(app *app, w http.ResponseWriter, r *http.Request) ([]byte,
return data, filename, err return data, filename, err
} }
func viewExportFull(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) { func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
var err error var err error
filename := "" filename := ""
u := getUserSession(app, r) u := getUserSession(app, r)
@ -655,7 +655,7 @@ func viewExportFull(app *app, w http.ResponseWriter, r *http.Request) ([]byte, s
return data, filename, err return data, filename, err
} }
func viewMeAPI(app *app, w http.ResponseWriter, r *http.Request) error { func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
uObj := struct { uObj := struct {
ID int64 `json:"id,omitempty"` ID int64 `json:"id,omitempty"`
@ -679,7 +679,7 @@ func viewMeAPI(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.WriteSuccess(w, uObj, http.StatusOK) return impart.WriteSuccess(w, uObj, http.StatusOK)
} }
func viewMyPostsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
if !reqJSON { if !reqJSON {
return ErrBadRequestedType return ErrBadRequestedType
@ -710,7 +710,7 @@ func viewMyPostsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) e
return impart.WriteSuccess(w, p, http.StatusOK) return impart.WriteSuccess(w, p, http.StatusOK)
} }
func viewMyCollectionsAPI(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
if !reqJSON { if !reqJSON {
return ErrBadRequestedType return ErrBadRequestedType
@ -724,7 +724,7 @@ func viewMyCollectionsAPI(app *app, u *User, w http.ResponseWriter, r *http.Requ
return impart.WriteSuccess(w, p, http.StatusOK) return impart.WriteSuccess(w, p, http.StatusOK)
} }
func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
p, err := app.db.GetAnonymousPosts(u) p, err := app.db.GetAnonymousPosts(u)
if err != nil { if err != nil {
log.Error("unable to fetch anon posts: %v", err) log.Error("unable to fetch anon posts: %v", err)
@ -761,7 +761,7 @@ func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) err
return nil return nil
} }
func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
c, err := app.db.GetCollections(u) c, err := app.db.GetCollections(u)
if err != nil { if err != nil {
log.Error("unable to fetch collections: %v", err) log.Error("unable to fetch collections: %v", err)
@ -792,7 +792,7 @@ func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request)
return nil return nil
} }
func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
c, err := app.db.GetCollection(vars["collection"]) c, err := app.db.GetCollection(vars["collection"])
if err != nil { if err != nil {
@ -815,7 +815,7 @@ func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Reques
return nil return nil
} }
func updateSettings(app *app, w http.ResponseWriter, r *http.Request) error { func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
var s userSettings var s userSettings
@ -904,7 +904,7 @@ func updateSettings(app *app, w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func updatePassphrase(app *app, w http.ResponseWriter, r *http.Request) error { func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error {
accessToken := r.Header.Get("Authorization") accessToken := r.Header.Get("Authorization")
if accessToken == "" { if accessToken == "" {
return ErrNoAccessToken return ErrNoAccessToken
@ -943,7 +943,7 @@ func updatePassphrase(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.WriteSuccess(w, struct{}{}, http.StatusOK) return impart.WriteSuccess(w, struct{}{}, http.StatusOK)
} }
func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
var c *Collection var c *Collection
var err error var err error
vars := mux.Vars(r) vars := mux.Vars(r)
@ -994,7 +994,7 @@ func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error
return nil return nil
} }
func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
fullUser, err := app.db.GetUserByID(u.ID) fullUser, err := app.db.GetUserByID(u.ID)
if err != nil { if err != nil {
log.Error("Unable to get user for settings: %s", err) log.Error("Unable to get user for settings: %s", err)
@ -1025,7 +1025,7 @@ func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) err
return nil return nil
} }
func saveTempInfo(app *app, key, val string, r *http.Request, w http.ResponseWriter) error { func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error {
session, err := app.sessionStore.Get(r, "t") session, err := app.sessionStore.Get(r, "t")
if err != nil { if err != nil {
return ErrInternalCookieSession return ErrInternalCookieSession
@ -1039,7 +1039,7 @@ func saveTempInfo(app *app, key, val string, r *http.Request, w http.ResponseWri
return err return err
} }
func getTempInfo(app *app, key string, r *http.Request, w http.ResponseWriter) string { func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string {
session, err := app.sessionStore.Get(r, "t") session, err := app.sessionStore.Get(r, "t")
if err != nil { if err != nil {
return "" return ""

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -24,7 +24,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/go-sql-driver/mysql"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/activity/streams" "github.com/writeas/activity/streams"
"github.com/writeas/httpsig" "github.com/writeas/httpsig"
@ -63,7 +62,7 @@ func (ru *RemoteUser) AsPerson() *activitystreams.Person {
} }
} }
func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Request) error { func handleFetchCollectionActivities(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)
vars := mux.Vars(r) vars := mux.Vars(r)
@ -81,13 +80,14 @@ func handleFetchCollectionActivities(app *app, w http.ResponseWriter, r *http.Re
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
p := c.PersonObject() p := c.PersonObject()
return impart.RenderActivityJSON(w, p, http.StatusOK) return impart.RenderActivityJSON(w, p, http.StatusOK)
} }
func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Request) error { func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)
vars := mux.Vars(r) vars := mux.Vars(r)
@ -105,6 +105,7 @@ func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Reques
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
if app.cfg.App.SingleUser { if app.cfg.App.SingleUser {
if alias != c.Alias { if alias != c.Alias {
@ -139,7 +140,7 @@ func handleFetchCollectionOutbox(app *app, w http.ResponseWriter, r *http.Reques
return impart.RenderActivityJSON(w, ocp, http.StatusOK) return impart.RenderActivityJSON(w, ocp, http.StatusOK)
} }
func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Request) error { func handleFetchCollectionFollowers(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)
vars := mux.Vars(r) vars := mux.Vars(r)
@ -157,6 +158,7 @@ func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
accountRoot := c.FederatedAccount() accountRoot := c.FederatedAccount()
@ -184,7 +186,7 @@ func handleFetchCollectionFollowers(app *app, w http.ResponseWriter, r *http.Req
return impart.RenderActivityJSON(w, ocp, http.StatusOK) return impart.RenderActivityJSON(w, ocp, http.StatusOK)
} }
func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Request) error { func handleFetchCollectionFollowing(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)
vars := mux.Vars(r) vars := mux.Vars(r)
@ -202,6 +204,7 @@ func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
accountRoot := c.FederatedAccount() accountRoot := c.FederatedAccount()
@ -219,7 +222,7 @@ func handleFetchCollectionFollowing(app *app, w http.ResponseWriter, r *http.Req
return impart.RenderActivityJSON(w, ocp, http.StatusOK) return impart.RenderActivityJSON(w, ocp, http.StatusOK)
} }
func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request) error { func handleFetchCollectionInbox(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)
vars := mux.Vars(r) vars := mux.Vars(r)
@ -235,6 +238,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
// TODO: return Reject? // TODO: return Reject?
return err return err
} }
c.hostName = app.cfg.App.Host
if debugging { if debugging {
dump, err := httputil.DumpRequest(r, true) dump, err := httputil.DumpRequest(r, true)
@ -350,7 +354,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
log.Error("No to! %v", err) log.Error("No to! %v", err)
return return
} }
err = makeActivityPost(p, fullActor.Inbox, am) err = makeActivityPost(app.cfg.App.Host, p, fullActor.Inbox, am)
if err != nil { if err != nil {
log.Error("Unable to make activity POST: %v", err) log.Error("Unable to make activity POST: %v", err)
return return
@ -371,13 +375,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
// Add follower locally, since it wasn't found before // Add follower locally, since it wasn't found before
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox) res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
if err != nil { if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok { if !app.db.isDuplicateKeyErr(err) {
if mysqlErr.Number != mySQLErrDuplicateKey {
t.Rollback()
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
return
}
} else {
t.Rollback() t.Rollback()
log.Error("Couldn't add new remoteuser in DB: %v\n", err) log.Error("Couldn't add new remoteuser in DB: %v\n", err)
return return
@ -394,13 +392,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
// Add in key // Add in key
_, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM) _, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM)
if err != nil { if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok { if !app.db.isDuplicateKeyErr(err) {
if mysqlErr.Number != mySQLErrDuplicateKey {
t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err)
return
}
} else {
t.Rollback() t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err) log.Error("Couldn't add follower keys in DB: %v\n", err)
return return
@ -411,13 +403,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
// Add follow // Add follow
_, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID) _, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+app.db.now()+")", c.ID, followerID)
if err != nil { if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok { if !app.db.isDuplicateKeyErr(err) {
if mysqlErr.Number != mySQLErrDuplicateKey {
t.Rollback()
log.Error("Couldn't add follower in DB: %v\n", err)
return
}
} else {
t.Rollback() t.Rollback()
log.Error("Couldn't add follower in DB: %v\n", err) log.Error("Couldn't add follower in DB: %v\n", err)
return return
@ -442,7 +428,7 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
return nil return nil
} }
func makeActivityPost(p *activitystreams.Person, url string, m interface{}) error { func makeActivityPost(hostName string, p *activitystreams.Person, url string, m interface{}) error {
log.Info("POST %s", url) log.Info("POST %s", url)
b, err := json.Marshal(m) b, err := json.Marshal(m)
if err != nil { if err != nil {
@ -496,7 +482,7 @@ func makeActivityPost(p *activitystreams.Person, url string, m interface{}) erro
return nil return nil
} }
func resolveIRI(url string) ([]byte, error) { func resolveIRI(hostName, url string) ([]byte, error) {
log.Info("GET %s", url) log.Info("GET %s", url)
r, _ := http.NewRequest("GET", url, nil) r, _ := http.NewRequest("GET", url, nil)
@ -532,7 +518,7 @@ func resolveIRI(url string) ([]byte, error) {
return body, nil return body, nil
} }
func deleteFederatedPost(app *app, p *PublicPost, collID int64) error { func deleteFederatedPost(app *App, p *PublicPost, collID int64) error {
if debugging { if debugging {
log.Info("Deleting federated post!") log.Info("Deleting federated post!")
} }
@ -566,7 +552,7 @@ func deleteFederatedPost(app *app, p *PublicPost, collID int64) error {
na.CC = append(na.CC, f) na.CC = append(na.CC, f)
} }
err = makeActivityPost(actor, si, activitystreams.NewDeleteActivity(na)) err = makeActivityPost(app.cfg.App.Host, actor, si, activitystreams.NewDeleteActivity(na))
if err != nil { if err != nil {
log.Error("Couldn't delete post! %v", err) log.Error("Couldn't delete post! %v", err)
} }
@ -574,7 +560,7 @@ func deleteFederatedPost(app *app, p *PublicPost, collID int64) error {
return nil return nil
} }
func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error { func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error {
if debugging { if debugging {
if isUpdate { if isUpdate {
log.Info("Federating updated post!") log.Info("Federating updated post!")
@ -620,7 +606,7 @@ func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error {
activity.To = na.To activity.To = na.To
activity.CC = na.CC activity.CC = na.CC
} }
err = makeActivityPost(actor, si, activity) err = makeActivityPost(app.cfg.App.Host, actor, si, activity)
if err != nil { if err != nil {
log.Error("Couldn't post! %v", err) log.Error("Couldn't post! %v", err)
} }
@ -628,7 +614,7 @@ func federatePost(app *app, p *PublicPost, collID int64, isUpdate bool) error {
return nil return nil
} }
func getRemoteUser(app *app, actorID string) (*RemoteUser, error) { func getRemoteUser(app *App, actorID string) (*RemoteUser, error) {
u := RemoteUser{ActorID: actorID} u := RemoteUser{ActorID: actorID}
err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox) err := app.db.QueryRow("SELECT id, inbox, shared_inbox FROM remoteusers WHERE actor_id = ?", actorID).Scan(&u.ID, &u.Inbox, &u.SharedInbox)
switch { switch {
@ -642,7 +628,7 @@ func getRemoteUser(app *app, actorID string) (*RemoteUser, error) {
return &u, nil return &u, nil
} }
func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser, error) { func getActor(app *App, actorIRI string) (*activitystreams.Person, *RemoteUser, error) {
log.Info("Fetching actor %s locally", actorIRI) log.Info("Fetching actor %s locally", actorIRI)
actor := &activitystreams.Person{} actor := &activitystreams.Person{}
remoteUser, err := getRemoteUser(app, actorIRI) remoteUser, err := getRemoteUser(app, actorIRI)
@ -651,7 +637,7 @@ func getActor(app *app, actorIRI string) (*activitystreams.Person, *RemoteUser,
if iErr.Status == http.StatusNotFound { if iErr.Status == http.StatusNotFound {
// Fetch remote actor // Fetch remote actor
log.Info("Not found; fetching actor %s remotely", actorIRI) log.Info("Not found; fetching actor %s remotely", actorIRI)
actorResp, err := resolveIRI(actorIRI) actorResp, err := resolveIRI(app.cfg.App.Host, actorIRI)
if err != nil { if err != nil {
log.Error("Unable to get actor! %v", err) log.Error("Unable to get actor! %v", err)
return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."} return nil, nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't fetch actor."}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -96,7 +96,7 @@ func (c instanceContent) UpdatedFriendly() string {
return c.Updated.Format("January 2, 2006, 3:04 PM") return c.Updated.Format("January 2, 2006, 3:04 PM")
} }
func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
updateAppStats() updateAppStats()
p := struct { p := struct {
*UserPage *UserPage
@ -117,7 +117,7 @@ func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Reque
return nil return nil
} }
func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
p := struct { p := struct {
*UserPage *UserPage
Config config.AppCfg Config config.AppCfg
@ -157,7 +157,7 @@ func handleViewAdminUsers(app *app, u *User, w http.ResponseWriter, r *http.Requ
return nil return nil
} }
func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
username := vars["username"] username := vars["username"]
if username == "" { if username == "" {
@ -229,7 +229,7 @@ func handleViewAdminUser(app *app, u *User, w http.ResponseWriter, r *http.Reque
return nil return nil
} }
func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
p := struct { p := struct {
*UserPage *UserPage
Config config.AppCfg Config config.AppCfg
@ -287,7 +287,7 @@ func handleViewAdminPages(app *app, u *User, w http.ResponseWriter, r *http.Requ
return nil return nil
} }
func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
slug := vars["slug"] slug := vars["slug"]
if slug == "" { if slug == "" {
@ -329,7 +329,7 @@ func handleViewAdminPage(app *app, u *User, w http.ResponseWriter, r *http.Reque
return nil return nil
} }
func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["page"] id := vars["page"]
@ -347,33 +347,34 @@ func handleAdminUpdateSite(app *app, u *User, w http.ResponseWriter, r *http.Req
return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m} return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
} }
func handleAdminUpdateConfig(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
app.cfg.App.SiteName = r.FormValue("site_name") apper.App().cfg.App.SiteName = r.FormValue("site_name")
app.cfg.App.SiteDesc = r.FormValue("site_desc") apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
app.cfg.App.OpenRegistration = r.FormValue("open_registration") == "on" apper.App().cfg.App.Landing = r.FormValue("landing")
apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
mul, err := strconv.Atoi(r.FormValue("min_username_len")) mul, err := strconv.Atoi(r.FormValue("min_username_len"))
if err == nil { if err == nil {
app.cfg.App.MinUsernameLen = mul apper.App().cfg.App.MinUsernameLen = mul
} }
mb, err := strconv.Atoi(r.FormValue("max_blogs")) mb, err := strconv.Atoi(r.FormValue("max_blogs"))
if err == nil { if err == nil {
app.cfg.App.MaxBlogs = mb apper.App().cfg.App.MaxBlogs = mb
} }
app.cfg.App.Federation = r.FormValue("federation") == "on" apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
app.cfg.App.PublicStats = r.FormValue("public_stats") == "on" apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
app.cfg.App.Private = r.FormValue("private") == "on" apper.App().cfg.App.Private = r.FormValue("private") == "on"
app.cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on" apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
if app.cfg.App.LocalTimeline && app.timeline == nil { if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
log.Info("Initializing local timeline...") log.Info("Initializing local timeline...")
initLocalTimeline(app) initLocalTimeline(apper.App())
} }
app.cfg.App.UserInvites = r.FormValue("user_invites") apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
if app.cfg.App.UserInvites == "none" { if apper.App().cfg.App.UserInvites == "none" {
app.cfg.App.UserInvites = "" apper.App().cfg.App.UserInvites = ""
} }
m := "?cm=Configuration+saved." m := "?cm=Configuration+saved."
err = config.Save(app.cfg, app.cfgFile) err = apper.SaveConfig(apper.App().cfg)
if err != nil { if err != nil {
m = "?cm=" + err.Error() m = "?cm=" + err.Error()
} }
@ -418,7 +419,7 @@ func updateAppStats() {
sysStatus.NumGC = m.NumGC sysStatus.NumGC = m.NumGC
} }
func adminResetPassword(app *app, u *User, newPass string) error { func adminResetPassword(app *App, u *User, newPass string) error {
hashedPass, err := auth.HashPass([]byte(newPass)) hashedPass, err := auth.HashPass([]byte(newPass))
if err != nil { if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)} return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}

666
app.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -12,9 +12,9 @@ package writefreely
import ( import (
"database/sql" "database/sql"
"flag"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -25,18 +25,18 @@ import (
"syscall" "syscall"
"time" "time"
_ "github.com/go-sql-driver/mysql"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/writeas/go-strip-markdown" "github.com/writeas/go-strip-markdown"
"github.com/writeas/impart"
"github.com/writeas/web-core/auth" "github.com/writeas/web-core/auth"
"github.com/writeas/web-core/converter" "github.com/writeas/web-core/converter"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/author" "github.com/writeas/writefreely/author"
"github.com/writeas/writefreely/config" "github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/key"
"github.com/writeas/writefreely/migrations" "github.com/writeas/writefreely/migrations"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
) )
@ -57,27 +57,129 @@ var (
softwareVer = "0.9.0" softwareVer = "0.9.0"
// DEPRECATED VARS // DEPRECATED VARS
// TODO: pass app.cfg into GetCollection* calls so we can get these values
// from Collection methods and we no longer need these.
hostName string
isSingleUser bool isSingleUser bool
) )
type app struct { // App holds data and configuration for an individual WriteFreely instance.
type App struct {
router *mux.Router router *mux.Router
shttp *http.ServeMux
db *datastore db *datastore
cfg *config.Config cfg *config.Config
cfgFile string cfgFile string
keys *keychain keys *key.Keychain
sessionStore *sessions.CookieStore sessionStore *sessions.CookieStore
formDecoder *schema.Decoder formDecoder *schema.Decoder
timeline *localTimeline timeline *localTimeline
} }
// DB returns the App's datastore
func (app *App) DB() *datastore {
return app.db
}
// Router returns the App's router
func (app *App) Router() *mux.Router {
return app.router
}
// Config returns the App's current configuration.
func (app *App) Config() *config.Config {
return app.cfg
}
// SetConfig updates the App's Config to the given value.
func (app *App) SetConfig(cfg *config.Config) {
app.cfg = cfg
}
// SetKeys updates the App's Keychain to the given value.
func (app *App) SetKeys(k *key.Keychain) {
app.keys = k
}
// Apper is the interface for getting data into and out of a WriteFreely
// instance (or "App").
//
// App returns the App for the current instance.
//
// LoadConfig reads an app configuration into the App, returning any error
// encountered.
//
// SaveConfig persists the current App configuration.
//
// LoadKeys reads the App's encryption keys and loads them into its
// key.Keychain.
type Apper interface {
App() *App
LoadConfig() error
SaveConfig(*config.Config) error
LoadKeys() error
}
// App returns the App
func (app *App) App() *App {
return app
}
// LoadConfig loads and parses a config file.
func (app *App) LoadConfig() error {
log.Info("Loading %s configuration...", app.cfgFile)
cfg, err := config.Load(app.cfgFile)
if err != nil {
log.Error("Unable to load configuration: %v", err)
os.Exit(1)
return err
}
app.cfg = cfg
return nil
}
// SaveConfig saves the given Config to disk -- namely, to the App's cfgFile.
func (app *App) SaveConfig(c *config.Config) error {
return config.Save(c, app.cfgFile)
}
// LoadKeys reads all needed keys from disk into the App. In order to use the
// configured `Server.KeysParentDir`, you must call initKeyPaths(App) before
// this.
func (app *App) LoadKeys() error {
var err error
app.keys = &key.Keychain{}
if debugging {
log.Info(" %s", emailKeyPath)
}
app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath)
if err != nil {
return err
}
if debugging {
log.Info(" %s", cookieAuthKeyPath)
}
app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
if err != nil {
return err
}
if debugging {
log.Info(" %s", cookieKeyPath)
}
app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath)
if err != nil {
return err
}
return nil
}
// handleViewHome shows page at root path. Will be the Pad if logged in and the // handleViewHome shows page at root path. Will be the Pad if logged in and the
// catch-all landing page otherwise. // catch-all landing page otherwise.
func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error {
if app.cfg.App.SingleUser { if app.cfg.App.SingleUser {
// Render blog index // Render blog index
return handleViewCollection(app, w, r) return handleViewCollection(app, w, r)
@ -90,6 +192,10 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
return handleViewPad(app, w, r) return handleViewPad(app, w, r)
} }
if land := app.cfg.App.LandingPath(); land != "/" {
return impart.HTTPError{http.StatusFound, land}
}
p := struct { p := struct {
page.StaticPage page.StaticPage
Flashes []template.HTML Flashes []template.HTML
@ -112,7 +218,7 @@ func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
return renderPage(w, "landing.tmpl", p) return renderPage(w, "landing.tmpl", p)
} }
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error { func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *template.Template) error {
p := struct { p := struct {
page.StaticPage page.StaticPage
ContentTitle string ContentTitle string
@ -158,7 +264,7 @@ func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *te
return nil return nil
} }
func pageForReq(app *app, r *http.Request) page.StaticPage { func pageForReq(app *App, r *http.Request) page.StaticPage {
p := page.StaticPage{ p := page.StaticPage{
AppCfg: app.cfg.App, AppCfg: app.cfg.App,
Path: r.URL.Path, Path: r.URL.Path,
@ -187,277 +293,50 @@ func pageForReq(app *app, r *http.Request) page.StaticPage {
return p return p
} }
var shttp = http.NewServeMux()
var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$") var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
func Serve() { // Initialize loads the app configuration and initializes templates, keys,
// General options usable with other commands // session, route handlers, and the database connection.
debugPtr := flag.Bool("debug", false, "Enables debug logging.") func Initialize(apper Apper, debug bool) (*App, error) {
configFile := flag.String("c", "config.ini", "The configuration file to use") debugging = debug
// Setup actions apper.LoadConfig()
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
doConfig := flag.Bool("config", false, "Run the configuration process")
configSections := flag.String("sections", "server db app", "Which sections of the configuration to go through (requires --config), " +
"valid values are any combination of 'server', 'db' and 'app' " +
"example: writefreely --config --sections \"db app\"")
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
createSchema := flag.Bool("init-db", false, "Initialize app database")
migrate := flag.Bool("migrate", false, "Migrate the database")
// Admin actions // Load templates
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password") err := InitTemplates(apper.App().Config())
createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
outputVersion := flag.Bool("v", false, "Output the current version")
flag.Parse()
debugging = *debugPtr
app := &app{
cfgFile: *configFile,
}
if *outputVersion {
fmt.Println(serverSoftware + " " + softwareVer)
os.Exit(0)
} else if *createConfig {
log.Info("Creating configuration...")
c := config.New()
log.Info("Saving configuration %s...", app.cfgFile)
err := config.Save(c, app.cfgFile)
if err != nil {
log.Error("Unable to save configuration: %v", err)
os.Exit(1)
}
os.Exit(0)
} else if *doConfig {
if *configSections == "" {
*configSections = "server db app"
}
// let's check there aren't any garbage in the list
configSectionsArray := strings.Split(*configSections, " ")
for _, element := range configSectionsArray {
if element != "server" && element != "db" && element != "app" {
log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
os.Exit(1)
}
}
d, err := config.Configure(app.cfgFile, *configSections)
if err != nil {
log.Error("Unable to configure: %v", err)
os.Exit(1)
}
if d.User != nil {
app.cfg = d.Config
connectToDatabase(app)
defer shutdown(app)
if !app.db.DatabaseInitialized() {
err = adminInitDatabase(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
}
u := &User{
Username: d.User.Username,
HashedPass: d.User.HashedPass,
Created: time.Now().Truncate(time.Second).UTC(),
}
// Create blog
log.Info("Creating user %s...\n", u.Username)
err = app.db.CreateUser(u, app.cfg.App.SiteName)
if err != nil {
log.Error("Unable to create user: %s", err)
os.Exit(1)
}
log.Info("Done!")
}
os.Exit(0)
} else if *genKeys {
errStatus := 0
// Read keys path from config
loadConfig(app)
// Create keys dir if it doesn't exist yet
fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
err = os.Mkdir(fullKeysDir, 0700)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
}
// Generate keys
initKeyPaths(app)
err := generateKey(emailKeyPath)
if err != nil {
errStatus = 1
}
err = generateKey(cookieAuthKeyPath)
if err != nil {
errStatus = 1
}
err = generateKey(cookieKeyPath)
if err != nil {
errStatus = 1
}
os.Exit(errStatus)
} else if *createSchema {
loadConfig(app)
connectToDatabase(app)
defer shutdown(app)
err := adminInitDatabase(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *createAdmin != "" {
err := adminCreateUser(app, *createAdmin, true)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *createUser != "" {
err := adminCreateUser(app, *createUser, false)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *resetPassUser != "" {
// Connect to the database
loadConfig(app)
connectToDatabase(app)
defer shutdown(app)
// Fetch user
u, err := app.db.GetUserForAuth(*resetPassUser)
if err != nil {
log.Error("Get user: %s", err)
os.Exit(1)
}
// Prompt for new password
prompt := promptui.Prompt{
Templates: &promptui.PromptTemplates{
Success: "{{ . | bold | faint }}: ",
},
Label: "New password",
Mask: '*',
}
newPass, err := prompt.Run()
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
// Do the update
log.Info("Updating...")
err = adminResetPassword(app, u, newPass)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
log.Info("Success.")
os.Exit(0)
} else if *migrate {
loadConfig(app)
connectToDatabase(app)
defer shutdown(app)
err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
if err != nil {
log.Error("migrate: %s", err)
os.Exit(1)
}
os.Exit(0)
}
log.Info("Initializing...")
loadConfig(app)
hostName = app.cfg.App.Host
isSingleUser = app.cfg.App.SingleUser
app.cfg.Server.Dev = *debugPtr
err := initTemplates(app.cfg)
if err != nil { if err != nil {
log.Error("load templates: %s", err) return nil, fmt.Errorf("load templates: %s", err)
os.Exit(1)
} }
// Load keys // Load keys and set up session
log.Info("Loading encryption keys...") initKeyPaths(apper.App()) // TODO: find a better way to do this, since it's unneeded in all Apper implementations
initKeyPaths(app) err = InitKeys(apper)
err = initKeys(app)
if err != nil { if err != nil {
log.Error("\n%s\n", err) return nil, fmt.Errorf("init keys: %s", err)
} }
apper.App().InitSession()
// Initialize modules apper.App().InitDecoder()
app.sessionStore = initSession(app)
app.formDecoder = schema.NewDecoder()
app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
// Check database configuration err = ConnectToDatabase(apper.App())
if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
log.Error("Database user or password not set.")
os.Exit(1)
}
if app.cfg.Database.Host == "" {
app.cfg.Database.Host = "localhost"
}
if app.cfg.Database.Database == "" {
app.cfg.Database.Database = "writefreely"
}
connectToDatabase(app)
defer shutdown(app)
// Test database connection
err = app.db.Ping()
if err != nil { if err != nil {
log.Error("Database ping failed: %s", err) return nil, fmt.Errorf("connect to DB: %s", err)
} }
r := mux.NewRouter()
handler := NewHandler(app)
handler.SetErrorPages(&ErrorPages{
NotFound: pages["404-general.tmpl"],
Gone: pages["410.tmpl"],
InternalServerError: pages["500.tmpl"],
Blank: pages["blank.tmpl"],
})
// Handle app routes
initRoutes(handler, r, app.cfg, app.db)
// Handle local timeline, if enabled // Handle local timeline, if enabled
if app.cfg.App.LocalTimeline { if apper.App().cfg.App.LocalTimeline {
log.Info("Initializing local timeline...") log.Info("Initializing local timeline...")
initLocalTimeline(app) initLocalTimeline(apper.App())
} }
// Handle static files return apper.App(), nil
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir))) }
shttp.Handle("/", fs)
r.PathPrefix("/").Handler(fs) func Serve(app *App, r *mux.Router) {
log.Info("Going to serve...")
isSingleUser = app.cfg.App.SingleUser
app.cfg.Server.Dev = debugging
// Handle shutdown // Handle shutdown
c := make(chan os.Signal, 2) c := make(chan os.Signal, 2)
@ -470,13 +349,12 @@ func Serve() {
os.Exit(0) os.Exit(0)
}() }()
http.Handle("/", r)
// Start web application server // Start web application server
var bindAddress = app.cfg.Server.Bind var bindAddress = app.cfg.Server.Bind
if bindAddress == "" { if bindAddress == "" {
bindAddress = "localhost" bindAddress = "localhost"
} }
var err error
if app.cfg.IsSecureStandalone() { if app.cfg.IsSecureStandalone() {
log.Info("Serving redirects on http://%s:80", bindAddress) log.Info("Serving redirects on http://%s:80", bindAddress)
go func() { go func() {
@ -490,11 +368,11 @@ func Serve() {
log.Info("Serving on https://%s:443", bindAddress) log.Info("Serving on https://%s:443", bindAddress)
log.Info("---") log.Info("---")
err = http.ListenAndServeTLS( err = http.ListenAndServeTLS(
fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil) fmt.Sprintf("%s:443", bindAddress), app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, r)
} else { } else {
log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port) log.Info("Serving on http://%s:%d\n", bindAddress, app.cfg.Server.Port)
log.Info("---") log.Info("---")
err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), nil) err = http.ListenAndServe(fmt.Sprintf("%s:%d", bindAddress, app.cfg.Server.Port), r)
} }
if err != nil { if err != nil {
log.Error("Unable to start: %v", err) log.Error("Unable to start: %v", err)
@ -502,17 +380,216 @@ func Serve() {
} }
} }
func loadConfig(app *app) { func (app *App) InitDecoder() {
log.Info("Loading %s configuration...", app.cfgFile) // TODO: do this at the package level, instead of the App level
cfg, err := config.Load(app.cfgFile) // Initialize modules
if err != nil { app.formDecoder = schema.NewDecoder()
log.Error("Unable to load configuration: %v", err) app.formDecoder.RegisterConverter(converter.NullJSONString{}, converter.ConvertJSONNullString)
os.Exit(1) app.formDecoder.RegisterConverter(converter.NullJSONBool{}, converter.ConvertJSONNullBool)
} app.formDecoder.RegisterConverter(sql.NullString{}, converter.ConvertSQLNullString)
app.cfg = cfg app.formDecoder.RegisterConverter(sql.NullBool{}, converter.ConvertSQLNullBool)
app.formDecoder.RegisterConverter(sql.NullInt64{}, converter.ConvertSQLNullInt64)
app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
} }
func connectToDatabase(app *app) { // ConnectToDatabase validates and connects to the configured database, then
// tests the connection.
func ConnectToDatabase(app *App) error {
// Check database configuration
if app.cfg.Database.Type == driverMySQL && (app.cfg.Database.User == "" || app.cfg.Database.Password == "") {
return fmt.Errorf("Database user or password not set.")
}
if app.cfg.Database.Host == "" {
app.cfg.Database.Host = "localhost"
}
if app.cfg.Database.Database == "" {
app.cfg.Database.Database = "writefreely"
}
// TODO: check err
connectToDatabase(app)
// Test database connection
err := app.db.Ping()
if err != nil {
return fmt.Errorf("Database ping failed: %s", err)
}
return nil
}
// OutputVersion prints out the version of the application.
func OutputVersion() {
fmt.Println(serverSoftware + " " + softwareVer)
}
// NewApp creates a new app instance.
func NewApp(cfgFile string) *App {
return &App{
cfgFile: cfgFile,
}
}
// CreateConfig creates a default configuration and saves it to the app's cfgFile.
func CreateConfig(app *App) error {
log.Info("Creating configuration...")
c := config.New()
log.Info("Saving configuration %s...", app.cfgFile)
err := config.Save(c, app.cfgFile)
if err != nil {
return fmt.Errorf("Unable to save configuration: %v", err)
}
return nil
}
// DoConfig runs the interactive configuration process.
func DoConfig(app *App, configSections string) {
if configSections == "" {
configSections = "server db app"
}
// let's check there aren't any garbage in the list
configSectionsArray := strings.Split(configSections, " ")
for _, element := range configSectionsArray {
if element != "server" && element != "db" && element != "app" {
log.Error("Invalid argument to --sections. Valid arguments are only \"server\", \"db\" and \"app\"")
os.Exit(1)
}
}
d, err := config.Configure(app.cfgFile, configSections)
if err != nil {
log.Error("Unable to configure: %v", err)
os.Exit(1)
}
if d.User != nil {
app.cfg = d.Config
connectToDatabase(app)
defer shutdown(app)
if !app.db.DatabaseInitialized() {
err = adminInitDatabase(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
}
u := &User{
Username: d.User.Username,
HashedPass: d.User.HashedPass,
Created: time.Now().Truncate(time.Second).UTC(),
}
// Create blog
log.Info("Creating user %s...\n", u.Username)
err = app.db.CreateUser(u, app.cfg.App.SiteName)
if err != nil {
log.Error("Unable to create user: %s", err)
os.Exit(1)
}
log.Info("Done!")
}
os.Exit(0)
}
// GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir.
func GenerateKeyFiles(app *App) error {
// Read keys path from config
app.LoadConfig()
// Create keys dir if it doesn't exist yet
fullKeysDir := filepath.Join(app.cfg.Server.KeysParentDir, keysDir)
if _, err := os.Stat(fullKeysDir); os.IsNotExist(err) {
err = os.Mkdir(fullKeysDir, 0700)
if err != nil {
return err
}
}
// Generate keys
initKeyPaths(app)
// TODO: use something like https://github.com/hashicorp/go-multierror to return errors
var keyErrs error
err := generateKey(emailKeyPath)
if err != nil {
keyErrs = err
}
err = generateKey(cookieAuthKeyPath)
if err != nil {
keyErrs = err
}
err = generateKey(cookieKeyPath)
if err != nil {
keyErrs = err
}
return keyErrs
}
// CreateSchema creates all database tables needed for the application.
func CreateSchema(apper Apper) error {
apper.LoadConfig()
connectToDatabase(apper.App())
defer shutdown(apper.App())
err := adminInitDatabase(apper.App())
if err != nil {
return err
}
return nil
}
// Migrate runs all necessary database migrations.
func Migrate(app *App) error {
app.LoadConfig()
connectToDatabase(app)
defer shutdown(app)
err := migrations.Migrate(migrations.NewDatastore(app.db.DB, app.db.driverName))
if err != nil {
return fmt.Errorf("migrate: %s", err)
}
return nil
}
// ResetPassword runs the interactive password reset process.
func ResetPassword(app *App, username string) error {
// Connect to the database
app.LoadConfig()
connectToDatabase(app)
defer shutdown(app)
// Fetch user
u, err := app.db.GetUserForAuth(username)
if err != nil {
log.Error("Get user: %s", err)
os.Exit(1)
}
// Prompt for new password
prompt := promptui.Prompt{
Templates: &promptui.PromptTemplates{
Success: "{{ . | bold | faint }}: ",
},
Label: "New password",
Mask: '*',
}
newPass, err := prompt.Run()
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
// Do the update
log.Info("Updating...")
err = adminResetPassword(app, u, newPass)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
log.Info("Success.")
return nil
}
func connectToDatabase(app *App) {
log.Info("Connecting to %s database...", app.cfg.Database.Type) log.Info("Connecting to %s database...", app.cfg.Database.Type)
var db *sql.DB var db *sql.DB
@ -542,28 +619,20 @@ func connectToDatabase(app *app) {
app.db = &datastore{db, app.cfg.Database.Type} app.db = &datastore{db, app.cfg.Database.Type}
} }
func shutdown(app *app) { func shutdown(app *App) {
log.Info("Closing database connection...") log.Info("Closing database connection...")
app.db.Close() app.db.Close()
} }
func adminCreateUser(app *app, credStr string, isAdmin bool) error { // CreateUser creates a new admin or normal user from the given credentials.
func CreateUser(apper Apper, username, password string, isAdmin bool) error {
// Create an admin user with --create-admin // Create an admin user with --create-admin
creds := strings.Split(credStr, ":") apper.LoadConfig()
if len(creds) != 2 { connectToDatabase(apper.App())
c := "user" defer shutdown(apper.App())
if isAdmin {
c = "admin"
}
return fmt.Errorf("usage: writefreely --create-%s username:password", c)
}
loadConfig(app)
connectToDatabase(app)
defer shutdown(app)
// Ensure an admin / first user doesn't already exist // Ensure an admin / first user doesn't already exist
firstUser, _ := app.db.GetUserByID(1) firstUser, _ := apper.App().db.GetUserByID(1)
if isAdmin { if isAdmin {
// Abort if trying to create admin user, but one already exists // Abort if trying to create admin user, but one already exists
if firstUser != nil { if firstUser != nil {
@ -577,9 +646,6 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
} }
// Create the user // Create the user
username := creds[0]
password := creds[1]
// Normalize and validate username // Normalize and validate username
desiredUsername := username desiredUsername := username
username = getSlug(username, "") username = getSlug(username, "")
@ -589,8 +655,8 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
usernameDesc += " (originally: " + desiredUsername + ")" usernameDesc += " (originally: " + desiredUsername + ")"
} }
if !author.IsValidUsername(app.cfg, username) { if !author.IsValidUsername(apper.App().cfg, username) {
return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, app.cfg.App.MinUsernameLen) return fmt.Errorf("Username %s is invalid, reserved, or shorter than configured minimum length (%d characters).", usernameDesc, apper.App().cfg.App.MinUsernameLen)
} }
// Hash the password // Hash the password
@ -610,7 +676,7 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
userType = "admin" userType = "admin"
} }
log.Info("Creating %s %s...", userType, usernameDesc) log.Info("Creating %s %s...", userType, usernameDesc)
err = app.db.CreateUser(u, desiredUsername) err = apper.App().db.CreateUser(u, desiredUsername)
if err != nil { if err != nil {
return fmt.Errorf("Unable to create user: %s", err) return fmt.Errorf("Unable to create user: %s", err)
} }
@ -618,7 +684,7 @@ func adminCreateUser(app *app, credStr string, isAdmin bool) error {
return nil return nil
} }
func adminInitDatabase(app *app) error { func adminInitDatabase(app *App) error {
schemaFileName := "schema.sql" schemaFileName := "schema.sql"
if app.cfg.Database.Type == driverSQLite { if app.cfg.Database.Type == driverSQLite {
schemaFileName = "sqlite.sql" schemaFileName = "sqlite.sql"

105
bindata-lib.go Normal file
View file

@ -0,0 +1,105 @@
// +build wflib
package writefreely
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
func bindata_read(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
return buf.Bytes(), nil
}
var _schema_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x59\x5f\x6f\xa3\x38\x10\x7f\xef\xa7\xf0\xdb\xa6\x52\x23\x6d\x7a\xdd\xaa\xba\xd3\x3e\x64\x53\x76\x2f\xba\x94\xee\x25\x44\xba\x7d\x02\x03\x93\xd4\xaa\xb1\x91\x6d\x92\xe6\xdb\x9f\x8c\x49\x08\x86\x24\xd0\xdb\x3b\x71\x7d\x2a\xcc\x6f\x8c\xfd\x9b\x3f\x9e\x99\x0c\x87\x57\xc3\x21\x7a\xc4\x0a\x87\x58\xc2\xaf\x28\xd8\x0a\xa2\x60\x25\x00\xe8\x2e\xb8\x1a\x0e\xaf\xb4\x78\xf8\xce\x3f\xad\xac\xf5\x3d\x1c\x52\x40\x52\x89\x2c\x52\x99\x00\xb4\xe2\x02\xa9\xfc\x5d\x80\xa3\x08\xa4\x54\xfc\x15\x98\x34\xdf\x9b\xcc\x9d\xb1\xe7\x20\x6f\xfc\x65\xe6\xa0\xe9\x57\xe4\x3e\x7b\xc8\xf9\x6b\xba\xf0\x16\x16\x1a\x0d\xae\x10\x0a\xf2\x87\x00\x85\x84\x61\xb1\x1b\x8c\xee\xaf\x73\x05\x77\x39\x9b\xdd\x68\x71\x26\x41\xf8\x24\x0e\x10\x61\x6a\x60\x0b\x65\x16\xf3\x00\x29\xc2\x76\x5a\x3a\x2a\xa5\xe8\xd1\xf9\x3a\x5e\xce\x3c\xf4\xe1\xe3\x87\x1c\xc9\x19\xf8\x8a\x24\xd0\x0e\x1d\x09\xc0\x0a\xe2\x00\xc5\x58\x81\x56\xab\x43\x27\xcb\xf9\xdc\x71\x3d\xdf\x9b\x3e\x39\x0b\x6f\xfc\xf4\x3d\x57\x84\xb7\x94\x08\x90\x47\x8a\x7b\x7c\xf5\x40\x78\x0d\x4c\x05\x68\x83\x45\xf4\x82\xc5\xe0\xf6\xd3\xa7\xeb\x1a\xf2\xfb\x7c\xfa\x34\x9e\xff\x40\x7f\x38\x3f\xd0\xa0\xa0\xe9\xfa\xea\x1a\x39\xee\xb7\xa9\xeb\x7c\x9e\x32\xc6\x1f\xbf\x94\xfb\xf9\x7d\x3c\x5f\x38\xde\x67\x8a\x15\x61\xa3\xdf\xfe\x75\xb3\xa7\x69\xc4\x99\xd2\xa7\xb8\x6c\xf4\x12\x6b\x4c\xae\xcd\xb9\x3f\xfa\x2f\xb6\x4d\x0f\xd0\x04\x62\x92\x25\x0a\xde\x54\x7e\xb8\xf1\xc4\x73\xe6\x68\xe1\x78\x28\x53\xab\x07\x34\x79\x9e\xcd\xf4\x17\xf5\x83\x1f\x12\x66\x79\x4d\x1a\xbf\xcb\x80\x55\xce\x49\xdc\x2b\xc2\x13\xb2\x16\x58\x11\xde\x18\x68\x16\xc0\x10\xbd\x01\x21\x09\x67\x26\x78\x46\x23\x8b\x69\x03\x6f\x64\x29\x97\x0b\x90\x19\x55\x01\xca\x4d\xb0\x97\xf4\x85\x8f\x88\x53\x0a\x91\x3e\x2c\x56\x4a\x90\x30\x53\xd0\x22\xff\x34\x6a\x19\xae\x4a\xd1\xc9\x74\x73\xd0\x29\xdd\x77\x74\xfb\x60\x81\x36\x98\x66\x60\x85\x76\xdd\x7f\x93\xf0\xae\xe2\xc2\x49\x78\x57\xf3\xe2\xaa\x33\x56\xf7\x77\x73\xb4\x99\xde\xf8\x68\xb9\xc5\x57\xd8\x75\xb2\x46\x8e\x6f\x6d\x87\x34\x0b\x29\x89\xfc\x57\xd8\x05\x28\xa4\x3c\xb4\xa4\x82\x6c\xb0\x82\x13\xe2\x73\xa4\xf6\x90\xc8\x14\x4b\xb9\xe5\x22\xee\xc4\x66\xa9\xd4\x9e\xd2\x42\x25\x40\xb9\xd7\xde\x7f\xbc\xfe\x3f\xb3\x26\x20\x26\x02\x22\xd5\x89\xb5\x52\xc9\xb0\x96\x0a\xd8\xf8\x98\x12\x2c\x8f\xc2\xfd\xa3\x45\x4c\xc0\x60\x7b\x11\x54\x65\xef\x68\xdd\x1e\x52\xd7\x89\x32\x79\x74\xa1\x5b\x5e\x85\xc6\x4b\xef\xd9\x9f\xba\x93\xb9\xf3\xe4\xb8\x9e\xc9\x9f\x0d\x3c\xb5\x4f\x8d\xb5\x4a\x4a\x11\x45\x7f\x4e\xa6\x0d\x62\x90\x91\x20\xa9\xca\x2f\xcb\xc3\xfe\xee\x3b\xed\xaf\x5a\x99\xaa\x1d\x05\x5f\xbe\x00\x14\x17\xa8\x79\x9b\x7f\xa4\xb8\x51\x5b\xaf\x9c\xab\xae\xb8\x48\xf0\x51\xc9\xf8\x50\x2f\x18\x4d\xe6\x8b\x76\x8d\x35\xae\xa9\x82\xb7\xec\x4c\x35\xbd\x21\xb0\xf5\x23\x9e\xe9\xe2\xab\x41\x5e\xaf\x8d\xf4\xdb\xa5\x3b\xfd\x73\xe9\xe4\x2f\xf7\xf6\x1d\x04\x3d\xf3\xee\x94\xcb\x36\xa9\xc0\xc0\x4a\x8f\x2e\x9c\xc0\xee\x39\x68\xb6\xb6\x7c\xb8\x66\x88\x84\xc7\x64\xb5\xf3\x8b\xd6\xc6\xd4\xb9\xb7\x0d\x38\xed\x07\x3e\x4e\x53\xc0\x02\xb3\x08\x0a\xe8\x5d\x53\x67\xc2\xb8\x48\x4c\x73\x42\x31\x5b\x67\x78\xbd\x47\x37\xad\x2b\x14\xad\x38\xc1\x4f\xf0\x94\xda\x12\xcd\x97\x4a\xfd\x4b\x84\x31\x88\xfd\x94\x4b\x62\xa2\xeb\xe8\x8b\x4b\x77\x31\xfd\xe6\x3a\x8f\x0d\x8b\xef\x1b\x30\x5d\x95\x4a\x85\x93\xb4\x6d\x07\x76\xa8\xfc\x3b\x6b\x5e\x70\x7f\x3b\xdd\xfc\x93\xec\x70\xe8\x71\xba\x25\x82\x8e\xe1\x48\x62\xdf\x38\x6b\xbd\x78\xcc\xdf\xd7\x14\x4a\xa3\x0f\xca\xff\x6f\x0e\x6b\xe7\x98\xc2\x73\x0a\xd4\xde\x8f\x6e\x7a\xd5\x2b\x09\x48\xb8\x82\x15\xa7\x94\x6f\x5b\xc4\x7d\x15\x7e\xb2\x64\xaa\xf5\x4f\x46\xcf\xaf\x4c\x28\x6a\xa0\xd3\xa3\x84\xcb\x25\xbe\xf5\x81\x9e\xf1\xab\xb7\xd5\xae\xce\xb7\xf0\xf5\x21\x40\x7e\x75\x77\xe7\xf6\x6c\x1f\x70\x39\x3e\x8c\xc5\x0f\x1e\xdf\x7f\xb6\x3b\x51\x6d\xd7\x66\xc7\xec\x35\x16\x67\x91\xe2\x86\x8a\xd3\x56\x21\x2c\xe4\x6f\xe7\x00\xf2\x05\x0b\x88\xfd\x4b\xb8\xcb\xb6\xb1\xe2\x6f\x50\x6e\xaf\x37\x76\xd1\x24\x77\x99\x3d\x58\x78\x63\x9d\xb3\xe3\xcd\x86\x79\xc3\xfd\xdd\x7f\x34\x6e\xd8\x6f\xac\x97\x83\x06\xbd\x39\xc2\x36\xa4\x99\xf7\x8a\xd8\x2a\xe7\x6c\x8a\xab\x75\x4e\x7d\x44\x86\xdf\x74\x42\x90\x01\x92\x09\xa6\xf4\x64\x2d\x74\x36\xc9\xb7\x99\x0a\x13\x86\x23\x45\x36\xcd\xf3\xe9\x3e\xd1\xde\xd2\xd1\x3b\x76\x86\x5a\x85\xe1\x04\xde\xdd\x1c\x5e\x1a\x66\x54\x57\x32\x7c\x1d\x16\x32\x8f\xf5\x75\x20\xc1\x84\xe6\x5b\x2a\x7e\x9d\x68\x9c\xd3\xbf\xfb\xd7\x82\xcb\x59\xb0\xa4\x65\x50\xfe\xdf\xab\x28\x94\x26\xce\xe2\x53\x61\x78\x90\x17\xee\x90\x3f\xf9\x27\xc3\xf1\xe4\x7d\xdf\xfa\xcc\x7f\x07\x00\x00\xff\xff\xbe\x79\x68\xa8\x10\x1b\x00\x00")
func schema_sql() ([]byte, error) {
return bindata_read(
_schema_sql,
"schema.sql",
)
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
return f()
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
"schema.sql": schema_sql,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
}
return rv, nil
}
type _bintree_t struct {
Func func() ([]byte, error)
Children map[string]*_bintree_t
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"schema.sql": &_bintree_t{schema_sql, map[string]*_bintree_t{
}},
}}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -11,9 +11,135 @@
package main package main
import ( import (
"flag"
"fmt"
"github.com/gorilla/mux"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely" "github.com/writeas/writefreely"
"os"
"strings"
) )
func main() { func main() {
writefreely.Serve() // General options usable with other commands
debugPtr := flag.Bool("debug", false, "Enables debug logging.")
configFile := flag.String("c", "config.ini", "The configuration file to use")
// Setup actions
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
doConfig := flag.Bool("config", false, "Run the configuration process")
configSections := flag.String("sections", "server db app", "Which sections of the configuration to go through (requires --config), " +
"valid values are any combination of 'server', 'db' and 'app' " +
"example: writefreely --config --sections \"db app\"")
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
createSchema := flag.Bool("init-db", false, "Initialize app database")
migrate := flag.Bool("migrate", false, "Migrate the database")
// Admin actions
createAdmin := flag.String("create-admin", "", "Create an admin with the given username:password")
createUser := flag.String("create-user", "", "Create a regular user with the given username:password")
resetPassUser := flag.String("reset-pass", "", "Reset the given user's password")
outputVersion := flag.Bool("v", false, "Output the current version")
flag.Parse()
app := writefreely.NewApp(*configFile)
if *outputVersion {
writefreely.OutputVersion()
os.Exit(0)
} else if *createConfig {
err := writefreely.CreateConfig(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *doConfig {
writefreely.DoConfig(app, *configSections)
os.Exit(0)
} else if *genKeys {
err := writefreely.GenerateKeyFiles(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *createSchema {
err := writefreely.CreateSchema(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *createAdmin != "" {
username, password, err := userPass(*createAdmin, true)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
err = writefreely.CreateUser(app, username, password, true)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *createUser != "" {
username, password, err := userPass(*createUser, false)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
err = writefreely.CreateUser(app, username, password, false)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *resetPassUser != "" {
err := writefreely.ResetPassword(app, *resetPassUser)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
} else if *migrate {
err := writefreely.Migrate(app)
if err != nil {
log.Error(err.Error())
os.Exit(1)
}
os.Exit(0)
}
// Initialize the application
var err error
app, err = writefreely.Initialize(app, *debugPtr)
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
// Set app routes
r := mux.NewRouter()
writefreely.InitRoutes(app, r)
app.InitStaticRoutes(r)
// Serve the application
writefreely.Serve(app, r)
}
func userPass(credStr string, isAdmin bool) (user string, pass string, err error) {
creds := strings.Split(credStr, ":")
if len(creds) != 2 {
c := "user"
if isAdmin {
c = "admin"
}
err = fmt.Errorf("usage: writefreely --create-%s username:password", c)
return
}
user = creds[0]
pass = creds[1]
return
} }

View file

@ -54,7 +54,8 @@ type (
PublicOwner bool `datastore:"public_owner" json:"-"` PublicOwner bool `datastore:"public_owner" json:"-"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
db *datastore db *datastore
hostName string
} }
CollectionObj struct { CollectionObj struct {
Collection Collection
@ -211,10 +212,10 @@ func (c *Collection) DisplayCanonicalURL() string {
func (c *Collection) RedirectingCanonicalURL(isRedir bool) string { func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
if isSingleUser { if isSingleUser {
return hostName + "/" return c.hostName + "/"
} }
return fmt.Sprintf("%s/%s/", hostName, c.Alias) return fmt.Sprintf("%s/%s/", c.hostName, c.Alias)
} }
// PrevPageURL provides a full URL for the previous page of collection posts, // PrevPageURL provides a full URL for the previous page of collection posts,
@ -300,11 +301,11 @@ func (c *Collection) AvatarURL() string {
if !isAvatarChar(fl) { if !isAvatarChar(fl) {
return "" return ""
} }
return hostName + "/img/avatars/" + fl + ".png" return c.hostName + "/img/avatars/" + fl + ".png"
} }
func (c *Collection) FederatedAPIBase() string { func (c *Collection) FederatedAPIBase() string {
return hostName + "/" return c.hostName + "/"
} }
func (c *Collection) FederatedAccount() string { func (c *Collection) FederatedAccount() string {
@ -316,7 +317,7 @@ func (c *Collection) RenderMathJax() bool {
return c.db.CollectionHasAttribute(c.ID, "render_mathjax") return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
} }
func newCollection(app *app, w http.ResponseWriter, r *http.Request) error { func newCollection(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
alias := r.FormValue("alias") alias := r.FormValue("alias")
title := r.FormValue("title") title := r.FormValue("title")
@ -399,7 +400,7 @@ func newCollection(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.HTTPError{http.StatusFound, redirectTo} return impart.HTTPError{http.StatusFound, redirectTo}
} }
func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (int64, error) { func apiCheckCollectionPermissions(app *App, r *http.Request, c *Collection) (int64, error) {
accessToken := r.Header.Get("Authorization") accessToken := r.Header.Get("Authorization")
var userID int64 = -1 var userID int64 = -1
if accessToken != "" { if accessToken != "" {
@ -419,7 +420,7 @@ func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (in
} }
// fetchCollection handles the API endpoint for retrieving collection data. // fetchCollection handles the API endpoint for retrieving collection data.
func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error { func fetchCollection(app *App, w http.ResponseWriter, r *http.Request) error {
accept := r.Header.Get("Accept") accept := r.Header.Get("Accept")
if strings.Contains(accept, "application/activity+json") { if strings.Contains(accept, "application/activity+json") {
return handleFetchCollectionActivities(app, w, r) return handleFetchCollectionActivities(app, w, r)
@ -434,6 +435,8 @@ func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
// Redirect users who aren't requesting JSON // Redirect users who aren't requesting JSON
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
if !reqJSON { if !reqJSON {
@ -467,7 +470,7 @@ func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
// fetchCollectionPosts handles an API endpoint for retrieving a collection's // fetchCollectionPosts handles an API endpoint for retrieving a collection's
// posts. // posts.
func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) error { func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
alias := vars["alias"] alias := vars["alias"]
@ -475,6 +478,7 @@ func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) erro
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
// Check permissions // Check permissions
userID, err := apiCheckCollectionPermissions(app, r, c) userID, err := apiCheckCollectionPermissions(app, r, c)
@ -563,7 +567,7 @@ func processCollectionRequest(cr *collectionReq, vars map[string]string, w http.
// domain that doesn't yet have a collection associated, or if a collection // domain that doesn't yet have a collection associated, or if a collection
// requires a password. In either case, this will return nil, nil -- thus both // requires a password. In either case, this will return nil, nil -- thus both
// values should ALWAYS be checked to determine whether or not to continue. // values should ALWAYS be checked to determine whether or not to continue.
func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) { func processCollectionPermissions(app *App, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) {
// Display collection if this is a collection // Display collection if this is a collection
var c *Collection var c *Collection
var err error var err error
@ -600,6 +604,7 @@ func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.R
} }
return nil, err return nil, err
} }
c.hostName = app.cfg.App.Host
// Update CollectionRequest to reflect owner status // Update CollectionRequest to reflect owner status
cr.isCollOwner = u != nil && u.ID == c.OwnerID cr.isCollOwner = u != nil && u.ID == c.OwnerID
@ -654,7 +659,7 @@ func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.R
return c, nil return c, nil
} }
func checkUserForCollection(app *app, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) { func checkUserForCollection(app *App, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) {
u := getUserSession(app, r) u := getUserSession(app, r)
return u, nil return u, nil
} }
@ -682,7 +687,7 @@ func getCollectionPage(vars map[string]string) int {
} }
// handleViewCollection displays the requested Collection // handleViewCollection displays the requested Collection
func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
cr := &collectionReq{} cr := &collectionReq{}
@ -788,7 +793,7 @@ func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) erro
return err return err
} }
func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
tag := vars["tag"] tag := vars["tag"]
@ -867,7 +872,7 @@ func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) e
return nil return nil
} }
func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Request) error { func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
slug := vars["slug"] slug := vars["slug"]
@ -885,7 +890,7 @@ func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Reque
return impart.HTTPError{http.StatusFound, loc} return impart.HTTPError{http.StatusFound, loc}
} }
func existingCollection(app *app, w http.ResponseWriter, r *http.Request) error { func existingCollection(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
vars := mux.Vars(r) vars := mux.Vars(r)
collAlias := vars["alias"] collAlias := vars["alias"]
@ -980,7 +985,7 @@ func collectionAliasFromReq(r *http.Request) string {
return alias return alias
} }
func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request) error { func handleWebCollectionUnlock(app *App, w http.ResponseWriter, r *http.Request) error {
var readReq struct { var readReq struct {
Alias string `schema:"alias" json:"alias"` Alias string `schema:"alias" json:"alias"`
Pass string `schema:"password" json:"password"` Pass string `schema:"password" json:"password"`
@ -1047,7 +1052,7 @@ func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request)
return impart.HTTPError{http.StatusFound, next} return impart.HTTPError{http.StatusFound, next}
} }
func isAuthorizedForCollection(app *app, alias string, r *http.Request) bool { func isAuthorizedForCollection(app *App, alias string, r *http.Request) bool {
authd := false authd := false
session, err := app.sessionStore.Get(r, blogPassCookieName) session, err := app.sessionStore.Get(r, blogPassCookieName)
if err == nil { if err == nil {

View file

@ -13,6 +13,7 @@ package config
import ( import (
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"strings"
) )
const ( const (
@ -64,6 +65,7 @@ type (
Theme string `ini:"theme"` Theme string `ini:"theme"`
JSDisabled bool `ini:"disable_js"` JSDisabled bool `ini:"disable_js"`
WebFonts bool `ini:"webfonts"` WebFonts bool `ini:"webfonts"`
Landing string `ini:"landing"`
// Users // Users
SingleUser bool `ini:"single_user"` SingleUser bool `ini:"single_user"`
@ -134,6 +136,13 @@ func (cfg *Config) IsSecureStandalone() bool {
return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != ""
} }
func (ac *AppCfg) LandingPath() string {
if !strings.HasPrefix(ac.Landing, "/") {
return "/" + ac.Landing
}
return ac.Landing
}
// Load reads the given configuration file, then parses and returns it as a Config. // Load reads the given configuration file, then parses and returns it as a Config.
func Load(fname string) (*Config, error) { func Load(fname string) (*Config, error) {
if fname == "" { if fname == "" {

20
database-lib.go Normal file
View file

@ -0,0 +1,20 @@
// +build wflib
/*
* Copyright © 2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
// This file contains dummy database funcs for when writefreely is used as a
// library.
package writefreely
func (db *datastore) isDuplicateKeyErr(err error) bool {
return false
}

View file

@ -1,4 +1,4 @@
// +build !sqlite // +build !sqlite,!wflib
/* /*
* Copyright © 2019 A Bunch Tell LLC. * Copyright © 2019 A Bunch Tell LLC.

View file

@ -1,4 +1,4 @@
// +build sqlite // +build sqlite,!wflib
/* /*
* Copyright © 2019 A Bunch Tell LLC. * Copyright © 2019 A Bunch Tell LLC.

View file

@ -29,6 +29,7 @@ import (
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/web-core/query" "github.com/writeas/web-core/query"
"github.com/writeas/writefreely/author" "github.com/writeas/writefreely/author"
"github.com/writeas/writefreely/key"
) )
const ( const (
@ -44,7 +45,7 @@ var (
type writestore interface { type writestore interface {
CreateUser(*User, string) error CreateUser(*User, string) error
UpdateUserEmail(keys *keychain, userID int64, email string) error UpdateUserEmail(keys *key.Keychain, userID int64, email string) error
UpdateEncryptedUserEmail(int64, []byte) error UpdateEncryptedUserEmail(int64, []byte) error
GetUserByID(int64) (*User, error) GetUserByID(int64) (*User, error)
GetUserForAuth(string) (*User, error) GetUserForAuth(string) (*User, error)
@ -60,7 +61,7 @@ type writestore interface {
GetTemporaryAccessToken(userID int64, validSecs int) (string, error) GetTemporaryAccessToken(userID int64, validSecs int) (string, error)
GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error) GetTemporaryOneTimeAccessToken(userID int64, validSecs int, oneTime bool) (string, error)
DeleteAccount(userID int64) (l *string, err error) DeleteAccount(userID int64) (l *string, err error)
ChangeSettings(app *app, u *User, s *userSettings) error ChangeSettings(app *App, u *User, s *userSettings) error
ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error
GetCollections(u *User) (*[]Collection, error) GetCollections(u *User) (*[]Collection, error)
@ -219,8 +220,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
// FIXME: We're returning errors inconsistently in this file. Do we use Errorf // FIXME: We're returning errors inconsistently in this file. Do we use Errorf
// for returned value, or impart? // for returned value, or impart?
func (db *datastore) UpdateUserEmail(keys *keychain, userID int64, email string) error { func (db *datastore) UpdateUserEmail(keys *key.Keychain, userID int64, email string) error {
encEmail, err := data.Encrypt(keys.emailKey, email) encEmail, err := data.Encrypt(keys.EmailKey, email)
if err != nil { if err != nil {
return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err) return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err)
} }
@ -1779,13 +1780,13 @@ func (db *datastore) GetUserPostsCount(userID int64) int64 {
// ChangeSettings takes a User and applies the changes in the given // ChangeSettings takes a User and applies the changes in the given
// userSettings, MODIFYING THE USER with successful changes. // userSettings, MODIFYING THE USER with successful changes.
func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error { func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error {
var errPass error var errPass error
q := query.NewUpdate() q := query.NewUpdate()
// Update email if given // Update email if given
if s.Email != "" { if s.Email != "" {
encEmail, err := data.Encrypt(app.keys.emailKey, s.Email) encEmail, err := data.Encrypt(app.keys.EmailKey, s.Email)
if err != nil { if err != nil {
log.Error("Couldn't encrypt email %s: %s\n", s.Email, err) log.Error("Couldn't encrypt email %s: %s\n", s.Email, err)
return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."} return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -99,7 +99,7 @@ func exportPostsZip(u *User, posts *[]PublicPost) []byte {
return b.Bytes() return b.Bytes()
} }
func compileFullExport(app *app, u *User) *ExportUser { func compileFullExport(app *App, u *User) *ExportUser {
exportUser := &ExportUser{ exportUser := &ExportUser{
User: u, User: u,
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -20,7 +20,7 @@ import (
"time" "time"
) )
func ViewFeed(app *app, w http.ResponseWriter, req *http.Request) error { func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error {
alias := collectionAliasFromReq(req) alias := collectionAliasFromReq(req)
// Display collection if this is a collection // Display collection if this is a collection
@ -34,6 +34,7 @@ func ViewFeed(app *app, w http.ResponseWriter, req *http.Request) error {
if err != nil { if err != nil {
return nil return nil
} }
c.hostName = app.cfg.App.Host
if c.IsPrivate() || c.IsProtected() { if c.IsPrivate() || c.IsProtected() {
return ErrCollectionNotFound return ErrCollectionNotFound

126
handle.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -36,16 +36,17 @@ const (
) )
type ( type (
handlerFunc func(app *app, w http.ResponseWriter, r *http.Request) error handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error
userHandlerFunc func(app *app, u *User, w http.ResponseWriter, r *http.Request) error userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error
dataHandlerFunc func(app *app, w http.ResponseWriter, r *http.Request) ([]byte, string, error) userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error
authFunc func(app *app, r *http.Request) (*User, error) dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
authFunc func(app *App, r *http.Request) (*User, error)
) )
type Handler struct { type Handler struct {
errors *ErrorPages errors *ErrorPages
sessionStore *sessions.CookieStore sessionStore *sessions.CookieStore
app *app app Apper
} }
// ErrorPages hold template HTML error pages for displaying errors to the user. // ErrorPages hold template HTML error pages for displaying errors to the user.
@ -59,7 +60,7 @@ type ErrorPages struct {
// NewHandler returns a new Handler instance, using the given StaticPage data, // NewHandler returns a new Handler instance, using the given StaticPage data,
// and saving alias to the application's CookieStore. // and saving alias to the application's CookieStore.
func NewHandler(app *app) *Handler { func NewHandler(apper Apper) *Handler {
h := &Handler{ h := &Handler{
errors: &ErrorPages{ errors: &ErrorPages{
NotFound: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>404</title></head><body><p>Not found.</p></body></html>{{end}}")), NotFound: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>404</title></head><body><p>Not found.</p></body></html>{{end}}")),
@ -67,13 +68,26 @@ func NewHandler(app *app) *Handler {
InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>500</title></head><body><p>Internal server error.</p></body></html>{{end}}")), InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>500</title></head><body><p>Internal server error.</p></body></html>{{end}}")),
Blank: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>{{.Title}}</title></head><body><p>{{.Content}}</p></body></html>{{end}}")), Blank: template.Must(template.New("").Parse("{{define \"base\"}}<html><head><title>{{.Title}}</title></head><body><p>{{.Content}}</p></body></html>{{end}}")),
}, },
sessionStore: app.sessionStore, sessionStore: apper.App().sessionStore,
app: app, app: apper,
} }
return h return h
} }
// NewWFHandler returns a new Handler instance, using WriteFreely template files.
// You MUST call writefreely.InitTemplates() before this.
func NewWFHandler(apper Apper) *Handler {
h := NewHandler(apper)
h.SetErrorPages(&ErrorPages{
NotFound: pages["404-general.tmpl"],
Gone: pages["410.tmpl"],
InternalServerError: pages["500.tmpl"],
Blank: pages["blank.tmpl"],
})
return h
}
// SetErrorPages sets the given set of ErrorPages as templates for any errors // SetErrorPages sets the given set of ErrorPages as templates for any errors
// that come up. // that come up.
func (h *Handler) SetErrorPages(e *ErrorPages) { func (h *Handler) SetErrorPages(e *ErrorPages) {
@ -91,21 +105,21 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
log.Error("%s: %s", e, debug.Stack()) log.Error("%s: %s", e, debug.Stack())
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = http.StatusInternalServerError status = http.StatusInternalServerError
} }
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}() }()
u := getUserSession(h.app, r) u := getUserSession(h.app.App(), r)
if u == nil { if u == nil {
err := ErrNotLoggedIn err := ErrNotLoggedIn
status = err.Status status = err.Status
return err return err
} }
err := f(h.app, u, w, r) err := f(h.app.App(), u, w, r)
if err == nil { if err == nil {
status = http.StatusOK status = http.StatusOK
} else if err, ok := err.(impart.HTTPError); ok { } else if err, ok := err.(impart.HTTPError); ok {
@ -129,14 +143,52 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
log.Error("%s: %s", e, debug.Stack()) log.Error("%s: %s", e, debug.Stack())
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = http.StatusInternalServerError status = http.StatusInternalServerError
} }
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()))
}() }()
u := getUserSession(h.app, r) u := getUserSession(h.app.App(), r)
if u == nil || !u.IsAdmin() {
err := impart.HTTPError{http.StatusNotFound, ""}
status = err.Status
return err
}
err := f(h.app.App(), u, w, r)
if err == nil {
status = http.StatusOK
} else if err, ok := err.(impart.HTTPError); ok {
status = err.Status
} else {
status = http.StatusInternalServerError
}
return err
}())
}
}
// AdminApper handles requests on /admin routes that require an Apper.
func (h *Handler) AdminApper(f userApperHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error {
var status int
start := time.Now()
defer func() {
if e := recover(); e != nil {
log.Error("%s: %s", e, debug.Stack())
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = http.StatusInternalServerError
}
log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()))
}()
u := getUserSession(h.app.App(), r)
if u == nil || !u.IsAdmin() { if u == nil || !u.IsAdmin() {
err := impart.HTTPError{http.StatusNotFound, ""} err := impart.HTTPError{http.StatusNotFound, ""}
status = err.Status status = err.Status
@ -160,7 +212,7 @@ func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc {
// UserAPI handles requests made in the API by the authenticated user. // UserAPI handles requests made in the API by the authenticated user.
// This provides user-friendly HTML pages and actions that work in the browser. // This provides user-friendly HTML pages and actions that work in the browser.
func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc { func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
return h.UserAll(false, f, func(app *app, r *http.Request) (*User, error) { return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) {
// Authorize user from Authorization header // Authorize user from Authorization header
t := r.Header.Get("Authorization") t := r.Header.Get("Authorization")
if t == "" { if t == "" {
@ -191,7 +243,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}() }()
u, err := a(h.app, r) u, err := a(h.app.App(), r)
if err != nil { if err != nil {
if err, ok := err.(impart.HTTPError); ok { if err, ok := err.(impart.HTTPError); ok {
status = err.Status status = err.Status
@ -201,7 +253,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
return err return err
} }
err = f(h.app, u, w, r) err = f(h.app.App(), u, w, r)
if err == nil { if err == nil {
status = 200 status = 200
} else if err, ok := err.(impart.HTTPError); ok { } else if err, ok := err.(impart.HTTPError); ok {
@ -222,7 +274,7 @@ func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerF
} }
func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc { func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
return func(app *app, w http.ResponseWriter, r *http.Request) error { return func(app *App, w http.ResponseWriter, r *http.Request) error {
err := f(app, w, r) err := f(app, w, r)
if err != nil { if err != nil {
if ie, ok := err.(impart.HTTPError); ok { if ie, ok := err.(impart.HTTPError); ok {
@ -239,7 +291,7 @@ func (h *Handler) RedirectOnErr(f handlerFunc, loc string) handlerFunc {
} }
func (h *Handler) Page(n string) http.HandlerFunc { func (h *Handler) Page(n string) http.HandlerFunc {
return h.Web(func(app *app, w http.ResponseWriter, r *http.Request) error { return h.Web(func(app *App, w http.ResponseWriter, r *http.Request) error {
t, ok := pages[n] t, ok := pages[n]
if !ok { if !ok {
return impart.HTTPError{http.StatusNotFound, "Page not found."} return impart.HTTPError{http.StatusNotFound, "Page not found."}
@ -264,13 +316,13 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
u := getUserSession(h.app, r) u := getUserSession(h.app.App(), r)
username := "None" username := "None"
if u != nil { if u != nil {
username = u.Username username = u.Username
} }
log.Error("User: %s\n\n%s: %s", username, e, debug.Stack()) log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = 500 status = 500
} }
@ -302,13 +354,13 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
} }
// TODO: pass User object to function // TODO: pass User object to function
err = f(h.app, w, r) err = f(h.app.App(), w, r)
if err == nil { if err == nil {
status = 200 status = 200
} else if httpErr, ok := err.(impart.HTTPError); ok { } else if httpErr, ok := err.(impart.HTTPError); ok {
status = httpErr.Status status = httpErr.Status
if status < 300 || status > 399 { if status < 300 || status > 399 {
addSessionFlash(h.app, w, r, httpErr.Message, session) addSessionFlash(h.app.App(), w, r, httpErr.Message, session)
return impart.HTTPError{http.StatusFound, r.Referer()} return impart.HTTPError{http.StatusFound, r.Referer()}
} }
} else { } else {
@ -319,7 +371,7 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
log.Error(e) log.Error(e)
} }
log.Info("Web handler internal error render") log.Info("Web handler internal error render")
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = 500 status = 500
} }
@ -338,14 +390,14 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
u := getUserSession(h.app, r) u := getUserSession(h.app.App(), r)
username := "None" username := "None"
if u != nil { if u != nil {
username = u.Username username = u.Username
} }
log.Error("User: %s\n\n%s: %s", username, e, debug.Stack()) log.Error("User: %s\n\n%s: %s", username, e, debug.Stack())
log.Info("Web deferred internal error render") log.Info("Web deferred internal error render")
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = 500 status = 500
} }
@ -375,7 +427,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
} }
// TODO: pass User object to function // TODO: pass User object to function
err := f(h.app, w, r) err := f(h.app.App(), w, r)
if err == nil { if err == nil {
status = 200 status = 200
} else if httpErr, ok := err.(impart.HTTPError); ok { } else if httpErr, ok := err.(impart.HTTPError); ok {
@ -384,7 +436,7 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
e := fmt.Sprintf("[Web handler] 500: %v", err) e := fmt.Sprintf("[Web handler] 500: %v", err)
log.Error(e) log.Error(e)
log.Info("Web internal error render") log.Info("Web internal error render")
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = 500 status = 500
} }
@ -412,7 +464,7 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
// TODO: do any needed authentication // TODO: do any needed authentication
err := f(h.app, w, r) err := f(h.app.App(), w, r)
if err != nil { if err != nil {
if err, ok := err.(impart.HTTPError); ok { if err, ok := err.(impart.HTTPError); ok {
status = err.Status status = err.Status
@ -434,14 +486,14 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
log.Error("%s: %s", e, debug.Stack()) log.Error("%s: %s", e, debug.Stack())
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = 500 status = 500
} }
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}() }()
data, filename, err := f(h.app, w, r) data, filename, err := f(h.app.App(), w, r)
if err != nil { if err != nil {
if err, ok := err.(impart.HTTPError); ok { if err, ok := err.(impart.HTTPError); ok {
status = err.Status status = err.Status
@ -530,7 +582,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
page.StaticPage page.StaticPage
Content *template.HTML Content *template.HTML
}{ }{
StaticPage: pageForReq(h.app, r), StaticPage: pageForReq(h.app.App(), r),
} }
if err.Message != "" { if err.Message != "" {
co := template.HTML(err.Message) co := template.HTML(err.Message)
@ -540,12 +592,12 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
return return
} else if err.Status == http.StatusNotFound { } else if err.Status == http.StatusNotFound {
w.WriteHeader(err.Status) w.WriteHeader(err.Status)
h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.NotFound.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
return return
} else if err.Status == http.StatusInternalServerError { } else if err.Status == http.StatusInternalServerError {
w.WriteHeader(err.Status) w.WriteHeader(err.Status)
log.Info("handleHTTPErorr internal error render") log.Info("handleHTTPErorr internal error render")
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
return return
} else if err.Status == http.StatusAccepted { } else if err.Status == http.StatusAccepted {
impart.WriteSuccess(w, "", err.Status) impart.WriteSuccess(w, "", err.Status)
@ -556,7 +608,7 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er
Title string Title string
Content template.HTML Content template.HTML
}{ }{
pageForReq(h.app, r), pageForReq(h.app.App(), r),
fmt.Sprintf("Uh oh (%d)", err.Status), fmt.Sprintf("Uh oh (%d)", err.Status),
template.HTML(fmt.Sprintf("<p style=\"text-align: center\" class=\"introduction\">%s</p>", err.Message)), template.HTML(fmt.Sprintf("<p style=\"text-align: center\" class=\"introduction\">%s</p>", err.Message)),
} }
@ -591,7 +643,7 @@ func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error)
impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."}) impart.WriteError(w, impart.HTTPError{http.StatusInternalServerError, "This is an unhelpful error message for a miscellaneous internal error."})
return return
} }
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
} }
func correctPageFromLoginAttempt(r *http.Request) string { func correctPageFromLoginAttempt(r *http.Request) string {
@ -613,7 +665,7 @@ func (h *Handler) LogHandlerFunc(f http.HandlerFunc) http.HandlerFunc {
defer func() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack()) log.Error("Handler.LogHandlerFunc\n\n%s: %s", e, debug.Stack())
h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r))
status = 500 status = 500
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -15,7 +15,7 @@ import (
"net/http" "net/http"
) )
func handleViewHostMeta(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewHostMeta(app *App, w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Server", serverSoftware) w.Header().Set("Server", serverSoftware)
w.Header().Set("Content-Type", "application/xrd+xml; charset=utf-8") w.Header().Set("Content-Type", "application/xrd+xml; charset=utf-8")

View file

@ -45,7 +45,7 @@ func (i Invite) ExpiresFriendly() string {
return i.Expires.Format("January 2, 2006, 3:04 PM") return i.Expires.Format("January 2, 2006, 3:04 PM")
} }
func handleViewUserInvites(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleViewUserInvites(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
// Don't show page if instance doesn't allow it // Don't show page if instance doesn't allow it
if !(app.cfg.App.UserInvites != "" && (u.IsAdmin() || app.cfg.App.UserInvites != "admin")) { if !(app.cfg.App.UserInvites != "" && (u.IsAdmin() || app.cfg.App.UserInvites != "admin")) {
return impart.HTTPError{http.StatusNotFound, ""} return impart.HTTPError{http.StatusNotFound, ""}
@ -73,7 +73,7 @@ func handleViewUserInvites(app *app, u *User, w http.ResponseWriter, r *http.Req
return nil return nil
} }
func handleCreateUserInvite(app *app, u *User, w http.ResponseWriter, r *http.Request) error { func handleCreateUserInvite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
muVal := r.FormValue("uses") muVal := r.FormValue("uses")
expVal := r.FormValue("expires") expVal := r.FormValue("expires")
@ -106,7 +106,7 @@ func handleCreateUserInvite(app *app, u *User, w http.ResponseWriter, r *http.Re
return impart.HTTPError{http.StatusFound, "/me/invites"} return impart.HTTPError{http.StatusFound, "/me/invites"}
} }
func handleViewInvite(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error {
inviteCode := mux.Vars(r)["code"] inviteCode := mux.Vars(r)["code"]
i, err := app.db.GetUserInvite(inviteCode) i, err := app.db.GetUserInvite(inviteCode)

63
key/key.go Normal file
View file

@ -0,0 +1,63 @@
/*
* Copyright © 2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
// Package key holds application keys and utilities around generating them.
package key
import (
"crypto/rand"
)
const (
EncKeysBytes = 32
)
type Keychain struct {
EmailKey, CookieAuthKey, CookieKey []byte
}
// GenerateKeys generates necessary keys for the app on the given Keychain,
// skipping any that already exist.
func (keys *Keychain) GenerateKeys() error {
// Generate keys only if they don't already exist
// TODO: use something like https://github.com/hashicorp/go-multierror to return errors
var err, keyErrs error
if len(keys.EmailKey) == 0 {
keys.EmailKey, err = GenerateBytes(EncKeysBytes)
if err != nil {
keyErrs = err
}
}
if len(keys.CookieAuthKey) == 0 {
keys.CookieAuthKey, err = GenerateBytes(EncKeysBytes)
if err != nil {
keyErrs = err
}
}
if len(keys.CookieKey) == 0 {
keys.CookieKey, err = GenerateBytes(EncKeysBytes)
if err != nil {
keyErrs = err
}
}
return keyErrs
}
// GenerateBytes returns securely generated random bytes.
func GenerateBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}

62
keys.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -11,8 +11,8 @@
package writefreely package writefreely
import ( import (
"crypto/rand"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/key"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -20,8 +20,6 @@ import (
const ( const (
keysDir = "keys" keysDir = "keys"
encKeysBytes = 32
) )
var ( var (
@ -30,47 +28,22 @@ var (
cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256")
) )
type keychain struct { // InitKeys loads encryption keys into memory via the given Apper interface
emailKey, cookieAuthKey, cookieKey []byte func InitKeys(apper Apper) error {
log.Info("Loading encryption keys...")
err := apper.LoadKeys()
if err != nil {
return err
}
return nil
} }
func initKeyPaths(app *app) { func initKeyPaths(app *App) {
emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath)
cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath) cookieAuthKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieAuthKeyPath)
cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath) cookieKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, cookieKeyPath)
} }
func initKeys(app *app) error {
var err error
app.keys = &keychain{}
if debugging {
log.Info(" %s", emailKeyPath)
}
app.keys.emailKey, err = ioutil.ReadFile(emailKeyPath)
if err != nil {
return err
}
if debugging {
log.Info(" %s", cookieAuthKeyPath)
}
app.keys.cookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath)
if err != nil {
return err
}
if debugging {
log.Info(" %s", cookieKeyPath)
}
app.keys.cookieKey, err = ioutil.ReadFile(cookieKeyPath)
if err != nil {
return err
}
return nil
}
// generateKey generates a key at the given path used for the encryption of // generateKey generates a key at the given path used for the encryption of
// certain user data. Because user data becomes unrecoverable without these // certain user data. Because user data becomes unrecoverable without these
// keys, this won't overwrite any existing key, and instead outputs a message. // keys, this won't overwrite any existing key, and instead outputs a message.
@ -85,7 +58,7 @@ func generateKey(path string) error {
} }
log.Info("Generating %s.", path) log.Info("Generating %s.", path)
b, err := generateBytes(encKeysBytes) b, err := key.GenerateBytes(key.EncKeysBytes)
if err != nil { if err != nil {
log.Error("FAILED. %s. Run writefreely --gen-keys again.", err) log.Error("FAILED. %s. Run writefreely --gen-keys again.", err)
return err return err
@ -98,14 +71,3 @@ func generateKey(path string) error {
log.Info("Success.") log.Info("Success.")
return nil return nil
} }
// generateBytes returns securely generated random bytes.
func generateBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
return nil, err
}
return b, nil
}

6
pad.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -19,7 +19,7 @@ import (
"strings" "strings"
) )
func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
action := vars["action"] action := vars["action"]
slug := vars["slug"] slug := vars["slug"]
@ -102,7 +102,7 @@ func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func handleViewMeta(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewMeta(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
action := vars["action"] action := vars["action"]
slug := vars["slug"] slug := vars["slug"]

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -18,7 +18,7 @@ import (
var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local) var defaultPageUpdatedTime = time.Date(2018, 11, 8, 12, 0, 0, 0, time.Local)
func getAboutPage(app *app) (*instanceContent, error) { func getAboutPage(app *App) (*instanceContent, error) {
c, err := app.db.GetDynamicContent("about") c, err := app.db.GetDynamicContent("about")
if err != nil { if err != nil {
return nil, err return nil, err
@ -40,7 +40,7 @@ func defaultAboutTitle(cfg *config.Config) sql.NullString {
return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true} return sql.NullString{String: "About " + cfg.App.SiteName, Valid: true}
} }
func getPrivacyPage(app *app) (*instanceContent, error) { func getPrivacyPage(app *App) (*instanceContent, error) {
c, err := app.db.GetDynamicContent("privacy") c, err := app.db.GetDynamicContent("privacy")
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -262,7 +262,7 @@ func (p *Post) HasTitleLink() bool {
return hasLink return hasLink
} }
func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
friendlyID := vars["post"] friendlyID := vars["post"]
@ -277,7 +277,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error {
return handleTemplatedPage(app, w, r, t) return handleTemplatedPage(app, w, r, t)
} else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" { } else if (strings.Contains(r.URL.Path, ".") && !isRaw && !isMarkdown) || r.URL.Path == "/robots.txt" || r.URL.Path == "/manifest.json" {
// Serve static file // Serve static file
shttp.ServeHTTP(w, r) app.shttp.ServeHTTP(w, r)
return nil return nil
} }
@ -468,7 +468,7 @@ func handleViewPost(app *app, w http.ResponseWriter, r *http.Request) error {
// /posts // /posts
// /posts?collection={alias} // /posts?collection={alias}
// ? /collections/{alias}/posts // ? /collections/{alias}/posts
func newPost(app *app, w http.ResponseWriter, r *http.Request) error { func newPost(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
vars := mux.Vars(r) vars := mux.Vars(r)
collAlias := vars["alias"] collAlias := vars["alias"]
@ -593,7 +593,7 @@ func newPost(app *app, w http.ResponseWriter, r *http.Request) error {
return response return response
} }
func existingPost(app *app, w http.ResponseWriter, r *http.Request) error { func existingPost(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
vars := mux.Vars(r) vars := mux.Vars(r)
postID := vars["post"] postID := vars["post"]
@ -717,7 +717,7 @@ func existingPost(app *app, w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func deletePost(app *app, w http.ResponseWriter, r *http.Request) error { func deletePost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
friendlyID := vars["post"] friendlyID := vars["post"]
editToken := r.FormValue("token") editToken := r.FormValue("token")
@ -834,7 +834,7 @@ func deletePost(app *app, w http.ResponseWriter, r *http.Request) error {
} }
// addPost associates a post with the authenticated user. // addPost associates a post with the authenticated user.
func addPost(app *app, w http.ResponseWriter, r *http.Request) error { func addPost(app *App, w http.ResponseWriter, r *http.Request) error {
var ownerID int64 var ownerID int64
// Authenticate user // Authenticate user
@ -883,7 +883,7 @@ func addPost(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.WriteSuccess(w, res, http.StatusOK) return impart.WriteSuccess(w, res, http.StatusOK)
} }
func dispersePost(app *app, w http.ResponseWriter, r *http.Request) error { func dispersePost(app *App, w http.ResponseWriter, r *http.Request) error {
var ownerID int64 var ownerID int64
// Authenticate user // Authenticate user
@ -927,7 +927,7 @@ type (
) )
// pinPost pins a post to a blog // pinPost pins a post to a blog
func pinPost(app *app, w http.ResponseWriter, r *http.Request) error { func pinPost(app *App, w http.ResponseWriter, r *http.Request) error {
var userID int64 var userID int64
// Authenticate user // Authenticate user
@ -985,7 +985,7 @@ func pinPost(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.WriteSuccess(w, res, http.StatusOK) return impart.WriteSuccess(w, res, http.StatusOK)
} }
func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error { func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error {
var collID int64 var collID int64
var coll *Collection var coll *Collection
var err error var err error
@ -996,6 +996,7 @@ func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return err return err
} }
coll.hostName = app.cfg.App.Host
_, err = apiCheckCollectionPermissions(app, r, coll) _, err = apiCheckCollectionPermissions(app, r, coll)
if err != nil { if err != nil {
return err return err
@ -1034,7 +1035,7 @@ func fetchPost(app *app, w http.ResponseWriter, r *http.Request) error {
return impart.WriteSuccess(w, p, http.StatusOK) return impart.WriteSuccess(w, p, http.StatusOK)
} }
func fetchPostProperty(app *app, w http.ResponseWriter, r *http.Request) error { func fetchPostProperty(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
p, err := app.db.GetPostProperty(vars["post"], 0, vars["property"]) p, err := app.db.GetPostProperty(vars["post"], 0, vars["property"])
if err != nil { if err != nil {
@ -1056,7 +1057,7 @@ func (p *Post) processPost() PublicPost {
func (p *PublicPost) CanonicalURL() string { func (p *PublicPost) CanonicalURL() string {
if p.Collection == nil || p.Collection.Alias == "" { if p.Collection == nil || p.Collection.Alias == "" {
return hostName + "/" + p.ID return p.Collection.hostName + "/" + p.ID
} }
return p.Collection.CanonicalURL() + p.Slug.String return p.Collection.CanonicalURL() + p.Slug.String
} }
@ -1087,7 +1088,7 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object {
if isSingleUser { if isSingleUser {
tagBaseURL = p.Collection.CanonicalURL() + "tag:" tagBaseURL = p.Collection.CanonicalURL() + "tag:"
} else { } else {
tagBaseURL = fmt.Sprintf("%s/%s/tag:", hostName, p.Collection.Alias) tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias)
} }
for _, t := range p.Tags { for _, t := range p.Tags {
o.Tag = append(o.Tag, activitystreams.Tag{ o.Tag = append(o.Tag, activitystreams.Tag{
@ -1132,7 +1133,7 @@ func (p *SubmittedPost) isFontValid() bool {
return valid return valid
} }
func getRawPost(app *app, friendlyID string) *RawPost { func getRawPost(app *App, friendlyID string) *RawPost {
var content, font, title string var content, font, title string
var isRTL sql.NullBool var isRTL sql.NullBool
var lang sql.NullString var lang sql.NullString
@ -1152,7 +1153,7 @@ func getRawPost(app *app, friendlyID string) *RawPost {
} }
// TODO; return a Post! // TODO; return a Post!
func getRawCollectionPost(app *app, slug, collAlias string) *RawPost { func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
var id, title, content, font string var id, title, content, font string
var isRTL sql.NullBool var isRTL sql.NullBool
var lang sql.NullString var lang sql.NullString
@ -1189,7 +1190,7 @@ func getRawCollectionPost(app *app, slug, collAlias string) *RawPost {
} }
} }
func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error { func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
slug := vars["slug"] slug := vars["slug"]
@ -1200,7 +1201,7 @@ func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error
if strings.Contains(r.URL.Path, ".") && !isRaw { if strings.Contains(r.URL.Path, ".") && !isRaw {
// Serve static file // Serve static file
shttp.ServeHTTP(w, r) app.shttp.ServeHTTP(w, r)
return nil return nil
} }
@ -1244,6 +1245,7 @@ func viewCollectionPost(app *app, w http.ResponseWriter, r *http.Request) error
} }
return err return err
} }
c.hostName = app.cfg.App.Host
// Check collection permissions // Check collection permissions
if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) { if c.IsPrivate() && (u == nil || u.ID != c.OwnerID) {

25
read.go
View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -49,20 +49,20 @@ type readPublication struct {
TotalPages int TotalPages int
} }
func initLocalTimeline(app *app) { func initLocalTimeline(app *App) {
app.timeline = &localTimeline{ app.timeline = &localTimeline{
postsPerPage: tlPostsPerPage, postsPerPage: tlPostsPerPage,
m: memo.New(app.db.FetchPublicPosts, 10*time.Minute), m: memo.New(app.FetchPublicPosts, 10*time.Minute),
} }
} }
// satisfies memo.Func // satisfies memo.Func
func (db *datastore) FetchPublicPosts() (interface{}, error) { func (app *App) FetchPublicPosts() (interface{}, error) {
// Finds all public posts and posts in a public collection published during the owner's active subscription period and within the last 3 months // Finds all public posts and posts in a public collection published during the owner's active subscription period and within the last 3 months
rows, err := db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated rows, err := app.db.Query(`SELECT p.id, alias, c.title, p.slug, p.title, p.content, p.text_appearance, p.language, p.rtl, p.created, p.updated
FROM collections c FROM collections c
LEFT JOIN posts p ON p.collection_id = c.id LEFT JOIN posts p ON p.collection_id = c.id
WHERE c.privacy = 1 AND (p.created >= ` + db.dateSub(3, "month") + ` AND p.created <= ` + db.now() + ` AND pinned_position IS NULL) WHERE c.privacy = 1 AND (p.created >= ` + app.db.dateSub(3, "month") + ` AND p.created <= ` + app.db.now() + ` AND pinned_position IS NULL)
ORDER BY p.created DESC`) ORDER BY p.created DESC`)
if err != nil { if err != nil {
log.Error("Failed selecting from posts: %v", err) log.Error("Failed selecting from posts: %v", err)
@ -82,6 +82,8 @@ func (db *datastore) FetchPublicPosts() (interface{}, error) {
log.Error("[READ] Unable to scan row, skipping: %v", err) log.Error("[READ] Unable to scan row, skipping: %v", err)
continue continue
} }
c.hostName = app.cfg.App.Host
isCollectionPost := alias.Valid isCollectionPost := alias.Valid
if isCollectionPost { if isCollectionPost {
c.Alias = alias.String c.Alias = alias.String
@ -108,7 +110,7 @@ func (db *datastore) FetchPublicPosts() (interface{}, error) {
return posts, nil return posts, nil
} }
func viewLocalTimelineAPI(app *app, w http.ResponseWriter, r *http.Request) error { func viewLocalTimelineAPI(app *App, w http.ResponseWriter, r *http.Request) error {
updateTimelineCache(app.timeline) updateTimelineCache(app.timeline)
skip, _ := strconv.Atoi(r.FormValue("skip")) skip, _ := strconv.Atoi(r.FormValue("skip"))
@ -121,7 +123,7 @@ func viewLocalTimelineAPI(app *app, w http.ResponseWriter, r *http.Request) erro
return impart.WriteSuccess(w, posts, http.StatusOK) return impart.WriteSuccess(w, posts, http.StatusOK)
} }
func viewLocalTimeline(app *app, w http.ResponseWriter, r *http.Request) error { func viewLocalTimeline(app *App, w http.ResponseWriter, r *http.Request) error {
if !app.cfg.App.LocalTimeline { if !app.cfg.App.LocalTimeline {
return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."} return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."}
} }
@ -153,7 +155,7 @@ func updateTimelineCache(tl *localTimeline) {
} }
} }
func showLocalTimeline(app *app, w http.ResponseWriter, r *http.Request, page int, author, tag string) error { func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page int, author, tag string) error {
updateTimelineCache(app.timeline) updateTimelineCache(app.timeline)
pl := len(*(app.timeline.posts)) pl := len(*(app.timeline.posts))
@ -226,7 +228,7 @@ func (c *readPublication) PrevPageURL(n int) string {
// handlePostIDRedirect handles a route where a post ID is given and redirects // handlePostIDRedirect handles a route where a post ID is given and redirects
// the user to the canonical post URL. // the user to the canonical post URL.
func handlePostIDRedirect(app *app, w http.ResponseWriter, r *http.Request) error { func handlePostIDRedirect(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
postID := vars["post"] postID := vars["post"]
p, err := app.db.GetPost(postID, 0) p, err := app.db.GetPost(postID, 0)
@ -244,12 +246,13 @@ func handlePostIDRedirect(app *app, w http.ResponseWriter, r *http.Request) erro
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
// Retrieve collection information and send user to canonical URL // Retrieve collection information and send user to canonical URL
return impart.HTTPError{http.StatusFound, c.CanonicalURL() + p.Slug.String} return impart.HTTPError{http.StatusFound, c.CanonicalURL() + p.Slug.String}
} }
func viewLocalTimelineFeed(app *app, w http.ResponseWriter, req *http.Request) error { func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) error {
if !app.cfg.App.LocalTimeline { if !app.cfg.App.LocalTimeline {
return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."} return impart.HTTPError{http.StatusNotFound, "Page doesn't exist."}
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -14,15 +14,30 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/go-webfinger" "github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"github.com/writefreely/go-nodeinfo" "github.com/writefreely/go-nodeinfo"
"net/http" "net/http"
"path/filepath"
"strings" "strings"
) )
func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) { // InitStaticRoutes adds routes for serving static files.
hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:] // TODO: this should just be a func, not method
if cfg.App.SingleUser { func (app *App) InitStaticRoutes(r *mux.Router) {
// Handle static files
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
app.shttp = http.NewServeMux()
app.shttp.Handle("/", fs)
r.PathPrefix("/").Handler(fs)
}
// InitRoutes adds dynamic routes for the given mux.Router.
func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// Create handler
handler := NewWFHandler(apper)
// Set up routes
hostSubroute := apper.App().cfg.App.Host[strings.Index(apper.App().cfg.App.Host, "://")+3:]
if apper.App().cfg.App.SingleUser {
hostSubroute = "{domain}" hostSubroute = "{domain}"
} else { } else {
if strings.HasPrefix(hostSubroute, "localhost") { if strings.HasPrefix(hostSubroute, "localhost") {
@ -30,7 +45,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
} }
} }
if cfg.App.SingleUser { if apper.App().cfg.App.SingleUser {
log.Info("Adding %s routes (single user)...", hostSubroute) log.Info("Adding %s routes (single user)...", hostSubroute)
} else { } else {
log.Info("Adding %s routes (multi-user)...", hostSubroute) log.Info("Adding %s routes (multi-user)...", hostSubroute)
@ -40,7 +55,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
write := r.PathPrefix("/").Subrouter() write := r.PathPrefix("/").Subrouter()
// Federation endpoint configurations // Federation endpoint configurations
wf := webfinger.Default(wfResolver{db, cfg}) wf := webfinger.Default(wfResolver{apper.App().db, apper.App().cfg})
wf.NoTLSHandler = nil wf.NoTLSHandler = nil
// Federation endpoints // Federation endpoints
@ -49,15 +64,15 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
// webfinger // webfinger
write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger))) write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger)))
// nodeinfo // nodeinfo
niCfg := nodeInfoConfig(db, cfg) niCfg := nodeInfoConfig(apper.App().db, apper.App().cfg)
ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db}) ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{apper.App().cfg, apper.App().db})
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
// Set up dyamic page handlers // Set up dyamic page handlers
// Handle auth // Handle auth
auth := write.PathPrefix("/api/auth/").Subrouter() auth := write.PathPrefix("/api/auth/").Subrouter()
if cfg.App.OpenRegistration { if apper.App().cfg.App.OpenRegistration {
auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST") auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST")
} }
auth.HandleFunc("/login", handler.All(login)).Methods("POST") auth.HandleFunc("/login", handler.All(login)).Methods("POST")
@ -130,7 +145,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET") write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET") write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET") write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
write.HandleFunc("/admin/update/config", handler.Admin(handleAdminUpdateConfig)).Methods("POST") write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST") write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
// Handle special pages first // Handle special pages first
@ -144,7 +159,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter()) RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter())
draftEditPrefix := "" draftEditPrefix := ""
if cfg.App.SingleUser { if apper.App().cfg.App.SingleUser {
draftEditPrefix = "/d" draftEditPrefix = "/d"
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
} else { } else {
@ -155,7 +170,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET") write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET") write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET")
// Collections // Collections
if cfg.App.SingleUser { if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter()) RouteCollections(handler, write.PathPrefix("/").Subrouter())
} else { } else {
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional)) write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional))
@ -165,6 +180,7 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
} }
write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional)) write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional))
write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional)) write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional))
return r
} }
func RouteCollections(handler *Handler, r *mux.Router) { func RouteCollections(handler *Handler, r *mux.Router) {

96
scripts/upgrade-server.sh Executable file
View file

@ -0,0 +1,96 @@
#! /bin/bash
###############################################################################
## writefreely update script ##
## ##
## WARNING: running this script will overwrite any modifed assets or ##
## template files. If you have any custom changes to these files you ##
## should back them up FIRST. ##
## ##
## This must be run from the web application root directory ##
## i.e. /var/www/writefreely, and operates under the assumption that you##
## have not installed the binary `writefreely` in another location. ##
###############################################################################
#
# Copyright © 2019 A Bunch Tell LLC.
#
# This file is part of WriteFreely.
#
# WriteFreely is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, included
# in the LICENSE file in this source code package.
#
# only execute as root, or use sudo
if [[ `id -u` -ne 0 ]]; then
echo "You must login as root, or execute this script with sudo"
exit 10
fi
# go ahead and check for the latest release on linux
echo "Checking for updates..."
url=`curl -s https://api.github.com/repos/writeas/writefreely/releases/latest | grep 'browser_' | grep linux | cut -d\" -f4`
# check current version
bin_output=`./writefreely -v`
if [ -z "$bin_output" ]; then
exit 1
fi
current=${bin_output:12:5}
echo "Current version is v$current"
# grab latest version number
IFS='/'
read -ra parts <<< "$url"
latest=${parts[-2]}
echo "Latest release is $latest"
IFS='.'
read -ra cv <<< "$current"
read -ra lv <<< "${latest#v}"
IFS=' '
tempdir=$(mktemp -d)
if [[ ${lv[0]} -gt ${cv[0]} ]]; then
echo "New major version available."
echo "Downloading..."
`wget -P $tempdir -q --show-progress $url`
elif [[ ${lv[0]} -eq ${cv[0]} ]] && [[ ${lv[1]} -gt ${cv[1]} ]]; then
echo "New minor version available."
echo "Downloading..."
`wget -P $tempdir -q --show-progress $url`
elif [[ ${lv[2]} -gt ${cv[2]} ]]; then
echo "New patch version available."
echo "Downloading..."
`wget -P $tempdir -q --show-progress $url`
else
echo "Up to date."
exit 0
fi
filename=${parts[-1]}
# extract
echo "Extracting files..."
tar -zxf $tempdir/$filename -C $tempdir
# copy files
echo "Copying files..."
cp -r $tempdir/{pages,static,templates,writefreely} .
# restart service
echo "Restarting writefreely systemd service..."
if `systemctl restart writefreely`; then
echo "Success, version has been upgraded to $latest."
else
echo "Upgrade complete, but failed to restart service."
exit 1
fi

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -27,24 +27,24 @@ const (
blogPassCookieName = "ub" blogPassCookieName = "ub"
) )
// initSession creates the cookie store. It depends on the keychain already // InitSession creates the cookie store. It depends on the keychain already
// being loaded. // being loaded.
func initSession(app *app) *sessions.CookieStore { func (app *App) InitSession() {
// Register complex data types we'll be storing in cookies // Register complex data types we'll be storing in cookies
gob.Register(&User{}) gob.Register(&User{})
// Create the cookie store // Create the cookie store
store := sessions.NewCookieStore(app.keys.cookieAuthKey, app.keys.cookieKey) store := sessions.NewCookieStore(app.keys.CookieAuthKey, app.keys.CookieKey)
store.Options = &sessions.Options{ store.Options = &sessions.Options{
Path: "/", Path: "/",
MaxAge: sessionLength, MaxAge: sessionLength,
HttpOnly: true, HttpOnly: true,
Secure: strings.HasPrefix(app.cfg.App.Host, "https://"), Secure: strings.HasPrefix(app.cfg.App.Host, "https://"),
} }
return store app.sessionStore = store
} }
func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { func getSessionFlashes(app *App, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) {
var err error var err error
if session == nil { if session == nil {
session, err = app.sessionStore.Get(r, cookieName) session, err = app.sessionStore.Get(r, cookieName)
@ -66,7 +66,7 @@ func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session
return f, nil return f, nil
} }
func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error { func addSessionFlash(app *App, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error {
var err error var err error
if session == nil { if session == nil {
session, err = app.sessionStore.Get(r, cookieName) session, err = app.sessionStore.Get(r, cookieName)
@ -82,7 +82,7 @@ func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string,
return nil return nil
} }
func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) { func getUserAndSession(app *App, r *http.Request) (*User, *sessions.Session) {
session, err := app.sessionStore.Get(r, cookieName) session, err := app.sessionStore.Get(r, cookieName)
if err == nil { if err == nil {
// Got the currently logged-in user // Got the currently logged-in user
@ -97,12 +97,12 @@ func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) {
return nil, nil return nil, nil
} }
func getUserSession(app *app, r *http.Request) *User { func getUserSession(app *App, r *http.Request) *User {
u, _ := getUserAndSession(app, r) u, _ := getUserAndSession(app, r)
return u return u
} }
func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error { func saveUserSession(app *App, r *http.Request, w http.ResponseWriter) error {
session, err := app.sessionStore.Get(r, cookieName) session, err := app.sessionStore.Get(r, cookieName)
if err != nil { if err != nil {
return ErrInternalCookieSession return ErrInternalCookieSession
@ -127,7 +127,7 @@ func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error {
return err return err
} }
func getFullUserSession(app *app, r *http.Request) *User { func getFullUserSession(app *App, r *http.Request) *User {
u := getUserSession(app, r) u := getUserSession(app, r)
if u == nil { if u == nil {
return nil return nil

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -34,7 +34,7 @@ func buildSitemap(host, alias string) *stm.Sitemap {
return sm return sm
} }
func handleViewSitemap(app *app, w http.ResponseWriter, r *http.Request) error { func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
// Determine canonical blog URL // Determine canonical blog URL
@ -57,6 +57,7 @@ func handleViewSitemap(app *app, w http.ResponseWriter, r *http.Request) error {
if err != nil { if err != nil {
return err return err
} }
c.hostName = app.cfg.App.Host
if !isSubdomain { if !isSubdomain {
pre += alias + "/" pre += alias + "/"

View file

@ -98,7 +98,8 @@ func initUserPage(parentDir, path, key string) {
)) ))
} }
func initTemplates(cfg *config.Config) error { // InitTemplates loads all template files from the configured parent dir.
func InitTemplates(cfg *config.Config) error {
log.Info("Loading templates...") log.Info("Loading templates...")
tmplFiles, err := ioutil.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir)) tmplFiles, err := ioutil.ReadDir(filepath.Join(cfg.Server.TemplatesParentDir, templatesDir))
if err != nil { if err != nil {

View file

@ -71,6 +71,8 @@ p.docs {
<dd>{{.Config.Host}}</dd> <dd>{{.Config.Host}}</dd>
<dt>User Mode</dt> <dt>User Mode</dt>
<dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd> <dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Landing Page</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt> <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd> <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd>
<dt><label for="min_username_len">Minimum Username Length</label></dt> <dt><label for="min_username_len">Minimum Username Length</label></dt>

View file

@ -1,5 +1,5 @@
/* /*
* Copyright © 2018 A Bunch Tell LLC. * Copyright © 2018-2019 A Bunch Tell LLC.
* *
* This file is part of WriteFreely. * This file is part of WriteFreely.
* *
@ -18,7 +18,7 @@ import (
"net/http" "net/http"
) )
func handleWebSignup(app *app, w http.ResponseWriter, r *http.Request) error { func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
// Get params // Get params
@ -67,7 +67,7 @@ func handleWebSignup(app *app, w http.ResponseWriter, r *http.Request) error {
// { "username": "asdf" } // { "username": "asdf" }
// result: { code: 204 } // result: { code: 204 }
func handleUsernameCheck(app *app, w http.ResponseWriter, r *http.Request) error { func handleUsernameCheck(app *App, w http.ResponseWriter, r *http.Request) error {
reqJSON := IsJSON(r.Header.Get("Content-Type")) reqJSON := IsJSON(r.Header.Get("Content-Type"))
// Get params // Get params
@ -112,7 +112,7 @@ func handleUsernameCheck(app *app, w http.ResponseWriter, r *http.Request) error
return impart.HTTPError{http.StatusConflict, "Username is already taken."} return impart.HTTPError{http.StatusConflict, "Username is already taken."}
} }
func getValidUsername(app *app, reqName, prevName string) (string, *impart.HTTPError) { func getValidUsername(app *App, reqName, prevName string) (string, *impart.HTTPError) {
// Check if username is okay // Check if username is okay
finalUsername := getSlug(reqName, "") finalUsername := getSlug(reqName, "")
if finalUsername == "" { if finalUsername == "" {

View file

@ -16,6 +16,7 @@ import (
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
"github.com/writeas/web-core/data" "github.com/writeas/web-core/data"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/key"
) )
type ( type (
@ -79,13 +80,13 @@ type (
// EmailClear decrypts and returns the user's email, caching it in the user // EmailClear decrypts and returns the user's email, caching it in the user
// object. // object.
func (u *User) EmailClear(keys *keychain) string { func (u *User) EmailClear(keys *key.Keychain) string {
if u.clearEmail != "" { if u.clearEmail != "" {
return u.clearEmail return u.clearEmail
} }
if u.Email.Valid && u.Email.String != "" { if u.Email.Valid && u.Email.String != "" {
email, err := data.Decrypt(keys.emailKey, []byte(u.Email.String)) email, err := data.Decrypt(keys.EmailKey, []byte(u.Email.String))
if err != nil { if err != nil {
log.Error("Error decrypting user email: %v", err) log.Error("Error decrypting user email: %v", err)
} else { } else {

View file

@ -37,6 +37,7 @@ func (wfr wfResolver) FindUser(username string, host, requestHost string, r []we
log.Error("Unable to get blog: %v", err) log.Error("Unable to get blog: %v", err)
return nil, err return nil, err
} }
c.hostName = wfr.cfg.App.Host
if wfr.cfg.App.SingleUser { if wfr.cfg.App.SingleUser {
// Ensure handle matches user-chosen one on single-user blogs // Ensure handle matches user-chosen one on single-user blogs
if username != c.Alias { if username != c.Alias {