Page MenuHomeMusing Studio

No OneTemporary

diff --git a/app.go b/app.go
index e061de4..d72ee49 100644
--- a/app.go
+++ b/app.go
@@ -1,434 +1,449 @@
package writefreely
import (
"database/sql"
"flag"
"fmt"
_ "github.com/go-sql-driver/mysql"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/gorilla/sessions"
"github.com/manifoldco/promptui"
"github.com/writeas/go-strip-markdown"
"github.com/writeas/web-core/converter"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/page"
)
const (
staticDir = "static/"
assumedTitleLen = 80
postsPerPage = 10
serverSoftware = "WriteFreely"
softwareURL = "https://writefreely.org"
)
// Software version can be set from git env using -ldflags
var softwareVer = "0.3"
var (
debugging bool
// 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
)
type app struct {
router *mux.Router
db *datastore
cfg *config.Config
keys *keychain
sessionStore *sessions.CookieStore
formDecoder *schema.Decoder
}
// handleViewHome shows page at root path. Will be the Pad if logged in and the
// catch-all landing page otherwise.
func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
if app.cfg.App.SingleUser {
// Render blog index
return handleViewCollection(app, w, r)
}
// Multi-user instance
u := getUserSession(app, r)
if u != nil {
// User is logged in, so show the Pad
return handleViewPad(app, w, r)
}
p := struct {
page.StaticPage
Flashes []template.HTML
}{
StaticPage: pageForReq(app, r),
}
// Get error messages
session, err := app.sessionStore.Get(r, cookieName)
if err != nil {
// Ignore this
log.Error("Unable to get session in handleViewHome; ignoring: %v", err)
}
flashes, _ := getSessionFlashes(app, w, r, session)
for _, flash := range flashes {
p.Flashes = append(p.Flashes, template.HTML(flash))
}
// Show landing page
return renderPage(w, "landing.tmpl", p)
}
func handleTemplatedPage(app *app, w http.ResponseWriter, r *http.Request, t *template.Template) error {
p := struct {
page.StaticPage
Content template.HTML
PlainContent string
Updated string
AboutStats *InstanceStats
}{
StaticPage: pageForReq(app, r),
}
if r.URL.Path == "/about" || r.URL.Path == "/privacy" {
var c string
var updated *time.Time
var err error
if r.URL.Path == "/about" {
c, err = getAboutPage(app)
// Fetch stats
p.AboutStats = &InstanceStats{}
p.AboutStats.NumPosts, _ = app.db.GetTotalPosts()
p.AboutStats.NumBlogs, _ = app.db.GetTotalCollections()
} else {
c, updated, err = getPrivacyPage(app)
}
if err != nil {
return err
}
p.Content = template.HTML(applyMarkdown([]byte(c)))
p.PlainContent = shortPostDescription(stripmd.Strip(c))
if updated != nil {
p.Updated = updated.Format("January 2, 2006")
}
}
// Serve templated page
err := t.ExecuteTemplate(w, "base", p)
if err != nil {
log.Error("Unable to render page: %v", err)
}
return nil
}
func pageForReq(app *app, r *http.Request) page.StaticPage {
p := page.StaticPage{
AppCfg: app.cfg.App,
Path: r.URL.Path,
Version: "v" + softwareVer,
}
// Add user information, if given
var u *User
accessToken := r.FormValue("t")
if accessToken != "" {
userID := app.db.GetUserID(accessToken)
if userID != -1 {
var err error
u, err = app.db.GetUserByID(userID)
if err == nil {
p.Username = u.Username
}
}
} else {
u = getUserSession(app, r)
if u != nil {
p.Username = u.Username
}
}
return p
}
var shttp = http.NewServeMux()
var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
func Serve() {
debugPtr := flag.Bool("debug", false, "Enables debug logging.")
createConfig := flag.Bool("create-config", false, "Creates a basic configuration and exits")
doConfig := flag.Bool("config", false, "Run the configuration process")
genKeys := flag.Bool("gen-keys", false, "Generate encryption and authentication keys")
createSchema := flag.Bool("init-db", false, "Initialize app database")
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{}
if *outputVersion {
fmt.Println(serverSoftware + " " + softwareVer)
os.Exit(0)
} else if *createConfig {
log.Info("Creating configuration...")
c := config.New()
log.Info("Saving configuration...")
err := config.Save(c)
if err != nil {
log.Error("Unable to save configuration: %v", err)
os.Exit(1)
}
os.Exit(0)
} else if *doConfig {
d, err := config.Configure()
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)
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
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 {
log.Info("Loading configuration...")
cfg, err := config.Load()
if err != nil {
log.Error("Unable to load configuration: %v", err)
os.Exit(1)
}
app.cfg = cfg
connectToDatabase(app)
defer shutdown(app)
schema, err := ioutil.ReadFile("schema.sql")
if err != nil {
log.Error("Unable to load schema.sql: %v", err)
os.Exit(1)
}
tblReg := regexp.MustCompile("CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`")
queries := strings.Split(string(schema), ";\n")
for _, q := range queries {
if strings.TrimSpace(q) == "" {
continue
}
parts := tblReg.FindStringSubmatch(q)
if len(parts) >= 3 {
log.Info("Creating table %s...", parts[2])
} else {
log.Info("Creating table ??? (Weird query) No match in: %v", parts)
}
_, err = app.db.Exec(q)
if err != nil {
log.Error("%s", err)
} else {
log.Info("Created.")
}
}
os.Exit(0)
} else if *resetPassUser != "" {
// Connect to the database
log.Info("Loading configuration...")
cfg, err := config.Load()
if err != nil {
log.Error("Unable to load configuration: %v", err)
os.Exit(1)
}
app.cfg = cfg
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)
}
log.Info("Initializing...")
log.Info("Loading configuration...")
cfg, err := config.Load()
if err != nil {
log.Error("Unable to load configuration: %v", err)
os.Exit(1)
}
app.cfg = cfg
hostName = cfg.App.Host
isSingleUser = cfg.App.SingleUser
app.cfg.Server.Dev = *debugPtr
initTemplates()
// Load keys
log.Info("Loading encryption keys...")
err = initKeys(app)
if err != nil {
log.Error("\n%s\n", err)
}
// Initialize modules
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
if 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)
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 static files
fs := http.FileServer(http.Dir(staticDir))
shttp.Handle("/", fs)
r.PathPrefix("/").Handler(fs)
// Handle shutdown
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Info("Shutting down...")
shutdown(app)
log.Info("Done.")
os.Exit(0)
}()
- // Start web application server
http.Handle("/", r)
- log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
- log.Info("---")
- err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil)
+
+ // Start web application server
+ if app.cfg.IsSecureStandalone() {
+ log.Info("Serving redirects on http://localhost:80")
+ go func() {
+ err = http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.Redirect(w, r, app.cfg.App.Host, http.StatusMovedPermanently)
+ }))
+ log.Error("Unable to start redirect server: %v", err)
+ }()
+
+ log.Info("Serving on https://localhost:443")
+ log.Info("---")
+ err = http.ListenAndServeTLS(":443", app.cfg.Server.TLSCertPath, app.cfg.Server.TLSKeyPath, nil)
+ } else {
+ log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
+ log.Info("---")
+ err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil)
+ }
if err != nil {
log.Error("Unable to start: %v", err)
os.Exit(1)
}
}
func connectToDatabase(app *app) {
if app.cfg.Database.Type != "mysql" {
log.Error("Invalid database type '%s'. Only 'mysql' is supported right now.", app.cfg.Database.Type)
os.Exit(1)
}
log.Info("Connecting to %s database...", app.cfg.Database.Type)
db, err := sql.Open(app.cfg.Database.Type, fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database, url.QueryEscape(time.Local.String())))
if err != nil {
log.Error("%s", err)
os.Exit(1)
}
app.db = &datastore{db}
app.db.SetMaxOpenConns(50)
}
func shutdown(app *app) {
log.Info("Closing database connection...")
app.db.Close()
}
diff --git a/config/config.go b/config/config.go
index c3c0628..56d8848 100644
--- a/config/config.go
+++ b/config/config.go
@@ -1,102 +1,109 @@
package config
import (
"gopkg.in/ini.v1"
)
const (
FileName = "config.ini"
)
type (
ServerCfg struct {
HiddenHost string `ini:"hidden_host"`
Port int `ini:"port"`
+ TLSCertPath string `ini:"tls_cert_path"`
+ TLSKeyPath string `ini:"tls_key_path"`
+
Dev bool `ini:"-"`
}
DatabaseCfg struct {
Type string `ini:"type"`
User string `ini:"username"`
Password string `ini:"password"`
Database string `ini:"database"`
Host string `ini:"host"`
Port int `ini:"port"`
}
AppCfg struct {
SiteName string `ini:"site_name"`
Host string `ini:"host"`
// Site appearance
Theme string `ini:"theme"`
JSDisabled bool `ini:"disable_js"`
WebFonts bool `ini:"webfonts"`
// Users
SingleUser bool `ini:"single_user"`
OpenRegistration bool `ini:"open_registration"`
MinUsernameLen int `ini:"min_username_len"`
MaxBlogs int `ini:"max_blogs"`
// Federation
Federation bool `ini:"federation"`
PublicStats bool `ini:"public_stats"`
Private bool `ini:"private"`
}
Config struct {
Server ServerCfg `ini:"server"`
Database DatabaseCfg `ini:"database"`
App AppCfg `ini:"app"`
}
)
func New() *Config {
return &Config{
Server: ServerCfg{
Port: 8080,
},
Database: DatabaseCfg{
Type: "mysql",
Host: "localhost",
Port: 3306,
},
App: AppCfg{
Host: "http://localhost:8080",
Theme: "write",
WebFonts: true,
SingleUser: true,
MinUsernameLen: 3,
MaxBlogs: 1,
Federation: true,
PublicStats: true,
},
}
}
+func (cfg *Config) IsSecureStandalone() bool {
+ return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != ""
+}
+
func Load() (*Config, error) {
cfg, err := ini.Load(FileName)
if err != nil {
return nil, err
}
// Parse INI file
uc := &Config{}
err = cfg.MapTo(uc)
if err != nil {
return nil, err
}
return uc, nil
}
func Save(uc *Config) error {
cfg := ini.Empty()
err := ini.ReflectFrom(cfg, uc)
if err != nil {
return err
}
return cfg.SaveTo(FileName)
}
diff --git a/config/setup.go b/config/setup.go
index 54fa961..6a2f780 100644
--- a/config/setup.go
+++ b/config/setup.go
@@ -1,255 +1,318 @@
package config
import (
"fmt"
"github.com/fatih/color"
"github.com/manifoldco/promptui"
"github.com/mitchellh/go-wordwrap"
"github.com/writeas/web-core/auth"
"strconv"
)
type SetupData struct {
User *UserCreation
Config *Config
}
func Configure() (*SetupData, error) {
data := &SetupData{}
var err error
data.Config, err = Load()
var action string
if err != nil {
fmt.Println("No configuration yet. Creating new.")
data.Config = New()
action = "generate"
} else {
fmt.Println("Configuration loaded.")
action = "update"
}
title := color.New(color.Bold, color.BgGreen).PrintFunc()
intro := color.New(color.Bold, color.FgWhite).PrintlnFunc()
fmt.Println()
intro(" ✍ Write Freely Configuration ✍")
fmt.Println()
fmt.Println(wordwrap.WrapString(" This quick configuration process will "+action+" the application's config file, "+FileName+".\n\n It validates your input along the way, so you can be sure any future errors aren't caused by a bad configuration. If you'd rather configure your server manually, instead run: writefreely --create-config and edit that file.", 75))
fmt.Println()
title(" Server setup ")
fmt.Println()
tmpls := &promptui.PromptTemplates{
Success: "{{ . | bold | faint }}: ",
}
selTmpls := &promptui.SelectTemplates{
Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`),
}
- prompt := promptui.Prompt{
- Templates: tmpls,
- Label: "Local port",
- Validate: validatePort,
- Default: fmt.Sprintf("%d", data.Config.Server.Port),
+ // Environment selection
+ selPrompt := promptui.Select{
+ Templates: selTmpls,
+ Label: "Environment",
+ Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"},
}
- port, err := prompt.Run()
+ _, envType, err := selPrompt.Run()
if err != nil {
return data, err
}
- data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number
+ isDevEnv := envType == "Development"
+ isStandalone := envType == "Production, standalone"
+
+ data.Config.Server.Dev = isDevEnv
+
+ var prompt promptui.Prompt
+ if isDevEnv || !isStandalone {
+ // Running in dev environment or behind reverse proxy; ask for port
+ prompt = promptui.Prompt{
+ Templates: tmpls,
+ Label: "Local port",
+ Validate: validatePort,
+ Default: fmt.Sprintf("%d", data.Config.Server.Port),
+ }
+ port, err := prompt.Run()
+ if err != nil {
+ return data, err
+ }
+ data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number
+ }
+
+ if isStandalone {
+ selPrompt = promptui.Select{
+ Templates: selTmpls,
+ Label: "Web server mode",
+ Items: []string{"Insecure (port 80)", "Secure (port 443)"},
+ }
+ sel, _, err := selPrompt.Run()
+ if err != nil {
+ return data, err
+ }
+ if sel == 0 {
+ data.Config.Server.Port = 80
+ data.Config.Server.TLSCertPath = ""
+ data.Config.Server.TLSKeyPath = ""
+ } else if sel == 1 {
+ data.Config.Server.Port = 443
+
+ prompt = promptui.Prompt{
+ Templates: tmpls,
+ Label: "Certificate path",
+ Validate: validateNonEmpty,
+ Default: data.Config.Server.TLSCertPath,
+ }
+ data.Config.Server.TLSCertPath, err = prompt.Run()
+ if err != nil {
+ return data, err
+ }
+
+ prompt = promptui.Prompt{
+ Templates: tmpls,
+ Label: "Key path",
+ Validate: validateNonEmpty,
+ Default: data.Config.Server.TLSKeyPath,
+ }
+ data.Config.Server.TLSKeyPath, err = prompt.Run()
+ if err != nil {
+ return data, err
+ }
+ }
+ } else {
+ data.Config.Server.TLSCertPath = ""
+ data.Config.Server.TLSKeyPath = ""
+ }
fmt.Println()
title(" Database setup ")
fmt.Println()
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Username",
Validate: validateNonEmpty,
Default: data.Config.Database.User,
}
data.Config.Database.User, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Password",
Validate: validateNonEmpty,
Default: data.Config.Database.Password,
Mask: '*',
}
data.Config.Database.Password, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Database name",
Validate: validateNonEmpty,
Default: data.Config.Database.Database,
}
data.Config.Database.Database, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Host",
Validate: validateNonEmpty,
Default: data.Config.Database.Host,
}
data.Config.Database.Host, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Port",
Validate: validatePort,
Default: fmt.Sprintf("%d", data.Config.Database.Port),
}
dbPort, err := prompt.Run()
if err != nil {
return data, err
}
data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number
fmt.Println()
title(" App setup ")
fmt.Println()
- selPrompt := promptui.Select{
+ selPrompt = promptui.Select{
Templates: selTmpls,
Label: "Site type",
Items: []string{"Single user blog", "Multi-user instance"},
}
_, usersType, err := selPrompt.Run()
if err != nil {
return data, err
}
data.Config.App.SingleUser = usersType == "Single user blog"
if data.Config.App.SingleUser {
data.User = &UserCreation{}
// prompt for username
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Admin username",
Validate: validateNonEmpty,
}
data.User.Username, err = prompt.Run()
if err != nil {
return data, err
}
// prompt for password
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Admin password",
Validate: validateNonEmpty,
}
newUserPass, err := prompt.Run()
if err != nil {
return data, err
}
data.User.HashedPass, err = auth.HashPass([]byte(newUserPass))
if err != nil {
return data, err
}
}
siteNameLabel := "Instance name"
if data.Config.App.SingleUser {
siteNameLabel = "Blog name"
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: siteNameLabel,
Validate: validateNonEmpty,
Default: data.Config.App.SiteName,
}
data.Config.App.SiteName, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Public URL",
Validate: validateDomain,
Default: data.Config.App.Host,
}
data.Config.App.Host, err = prompt.Run()
if err != nil {
return data, err
}
if !data.Config.App.SingleUser {
selPrompt = promptui.Select{
Templates: selTmpls,
Label: "Registration",
Items: []string{"Open", "Closed"},
}
_, regType, err := selPrompt.Run()
if err != nil {
return data, err
}
data.Config.App.OpenRegistration = regType == "Open"
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Max blogs per user",
Default: fmt.Sprintf("%d", data.Config.App.MaxBlogs),
}
maxBlogs, err := prompt.Run()
if err != nil {
return data, err
}
data.Config.App.MaxBlogs, _ = strconv.Atoi(maxBlogs) // Ignore error, as we've already validated number
}
selPrompt = promptui.Select{
Templates: selTmpls,
Label: "Federation",
Items: []string{"Enabled", "Disabled"},
}
_, fedType, err := selPrompt.Run()
if err != nil {
return data, err
}
data.Config.App.Federation = fedType == "Enabled"
if data.Config.App.Federation {
selPrompt = promptui.Select{
Templates: selTmpls,
Label: "Federation usage stats",
Items: []string{"Public", "Private"},
}
_, fedStatsType, err := selPrompt.Run()
if err != nil {
return data, err
}
data.Config.App.PublicStats = fedStatsType == "Public"
selPrompt = promptui.Select{
Templates: selTmpls,
Label: "Instance metadata privacy",
Items: []string{"Public", "Private"},
}
_, fedStatsType, err = selPrompt.Run()
if err != nil {
return data, err
}
data.Config.App.Private = fedStatsType == "Private"
}
return data, Save(data.Config)
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Apr 6, 3:03 AM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3188745

Event Timeline