2018-12-24 17:45:15 +00:00
/ *
2019-05-12 20:55:30 +00:00
* Copyright © 2018 - 2019 A Bunch Tell LLC .
2018-12-24 17:45:15 +00:00
*
* 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 .
* /
2018-12-31 06:05:26 +00:00
2018-10-15 18:44:15 +00:00
package writefreely
import (
2019-07-21 00:49:20 +00:00
"crypto/tls"
2018-10-17 02:31:27 +00:00
"database/sql"
2018-10-15 18:44:15 +00:00
"fmt"
2018-11-10 02:14:22 +00:00
"html/template"
2019-06-13 22:50:23 +00:00
"io/ioutil"
2018-10-15 18:44:15 +00:00
"net/http"
2018-11-14 22:47:58 +00:00
"net/url"
2018-10-15 18:44:15 +00:00
"os"
"os/signal"
2019-01-18 23:57:04 +00:00
"path/filepath"
2018-11-08 05:11:42 +00:00
"regexp"
2018-11-13 18:04:52 +00:00
"strings"
2018-10-15 18:44:15 +00:00
"syscall"
2018-11-10 03:16:13 +00:00
"time"
2018-10-15 18:44:15 +00:00
"github.com/gorilla/mux"
2018-11-08 06:31:01 +00:00
"github.com/gorilla/schema"
2018-10-15 18:44:15 +00:00
"github.com/gorilla/sessions"
2018-11-14 20:03:22 +00:00
"github.com/manifoldco/promptui"
2019-08-29 22:05:59 +00:00
stripmd "github.com/writeas/go-strip-markdown"
2019-05-13 00:11:53 +00:00
"github.com/writeas/impart"
2018-12-06 02:41:51 +00:00
"github.com/writeas/web-core/auth"
2018-11-08 06:31:01 +00:00
"github.com/writeas/web-core/converter"
2018-10-15 18:44:15 +00:00
"github.com/writeas/web-core/log"
2018-12-20 02:26:13 +00:00
"github.com/writeas/writefreely/author"
2018-10-15 18:44:15 +00:00
"github.com/writeas/writefreely/config"
2019-06-13 14:14:35 +00:00
"github.com/writeas/writefreely/key"
2019-01-17 18:53:03 +00:00
"github.com/writeas/writefreely/migrations"
2018-11-08 04:50:50 +00:00
"github.com/writeas/writefreely/page"
2019-07-21 00:49:20 +00:00
"golang.org/x/crypto/acme/autocert"
2018-10-15 18:44:15 +00:00
)
const (
2019-01-18 23:57:04 +00:00
staticDir = "static"
2018-11-08 06:31:01 +00:00
assumedTitleLen = 80
postsPerPage = 10
2018-10-17 22:57:37 +00:00
2018-11-08 18:37:42 +00:00
serverSoftware = "WriteFreely"
2018-10-17 22:57:37 +00:00
softwareURL = "https://writefreely.org"
2018-10-15 18:44:15 +00:00
)
2018-11-08 03:13:16 +00:00
var (
debugging bool
2018-11-08 06:31:01 +00:00
2018-11-26 13:37:06 +00:00
// Software version can be set from git env using -ldflags
2020-03-27 16:53:48 +00:00
softwareVer = "0.12.0"
2018-11-26 13:37:06 +00:00
2018-11-08 06:31:01 +00:00
// DEPRECATED VARS
isSingleUser bool
2018-11-08 03:13:16 +00:00
)
2019-05-12 20:55:30 +00:00
// App holds data and configuration for an individual WriteFreely instance.
type App struct {
2018-10-15 18:44:15 +00:00
router * mux . Router
2019-06-13 22:22:18 +00:00
shttp * http . ServeMux
2018-10-17 02:31:27 +00:00
db * datastore
2018-10-15 18:44:15 +00:00
cfg * config . Config
2018-12-08 22:49:19 +00:00
cfgFile string
2019-06-13 14:14:35 +00:00
keys * key . Keychain
2019-12-19 16:48:04 +00:00
sessionStore sessions . Store
2018-11-08 06:31:01 +00:00
formDecoder * schema . Decoder
2019-08-29 22:05:59 +00:00
updates * updatesCache
2018-12-10 21:02:42 +00:00
timeline * localTimeline
2018-10-15 18:44:15 +00:00
}
2019-06-13 22:50:23 +00:00
// 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.
2019-06-13 14:14:35 +00:00
func ( app * App ) SetKeys ( k * key . Keychain ) {
app . keys = k
}
2019-12-19 16:48:04 +00:00
func ( app * App ) SessionStore ( ) sessions . Store {
return app . sessionStore
}
func ( app * App ) SetSessionStore ( s sessions . Store ) {
app . sessionStore = s
}
2019-06-13 22:50:23 +00:00
// 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
2019-08-01 20:12:22 +00:00
ReqLog ( r * http . Request , status int , timeSince time . Duration ) string
2019-06-13 22:50:23 +00:00
}
// 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
}
2019-08-01 20:12:22 +00:00
func ( app * App ) ReqLog ( r * http . Request , status int , timeSince time . Duration ) string {
return fmt . Sprintf ( "\"%s %s\" %d %s \"%s\"" , r . Method , r . RequestURI , status , timeSince , r . UserAgent ( ) )
}
2019-08-09 15:32:53 +00:00
// handleViewHome shows page at root path. It checks the configuration and
// authentication state to show the correct page.
2019-05-12 20:55:30 +00:00
func handleViewHome ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 05:11:42 +00:00
if app . cfg . App . SingleUser {
// Render blog index
return handleViewCollection ( app , w , r )
}
// Multi-user instance
2019-06-27 20:38:24 +00:00
forceLanding := r . FormValue ( "landing" ) == "1"
if ! forceLanding {
// Show correct page based on user auth status and configured landing path
u := getUserSession ( app , r )
2019-08-09 18:57:09 +00:00
if app . cfg . App . Chorus {
// This instance is focused on reading, so show Reader on home route if not
// private or a private-instance user is logged in.
if ! app . cfg . App . Private || u != nil {
return viewLocalTimeline ( app , w , r )
}
}
2019-06-27 20:38:24 +00:00
if u != nil {
// User is logged in, so show the Pad
return handleViewPad ( app , w , r )
}
2020-03-02 22:32:04 +00:00
if app . cfg . App . Private {
return viewLogin ( app , w , r )
}
2019-06-27 20:38:24 +00:00
if land := app . cfg . App . LandingPath ( ) ; land != "/" {
return impart . HTTPError { http . StatusFound , land }
}
2019-05-13 00:11:53 +00:00
}
2019-08-09 16:00:46 +00:00
return handleViewLanding ( app , w , r )
}
func handleViewLanding ( app * App , w http . ResponseWriter , r * http . Request ) error {
forceLanding := r . FormValue ( "landing" ) == "1"
2018-11-10 02:14:22 +00:00
p := struct {
page . StaticPage
2020-08-19 17:31:07 +00:00
* OAuthButtons
2018-11-10 02:14:22 +00:00
Flashes [ ] template . HTML
2019-06-27 21:06:37 +00:00
Banner template . HTML
Content template . HTML
2019-06-27 20:38:24 +00:00
ForcedLanding bool
2018-11-10 02:14:22 +00:00
} {
2020-08-19 17:31:07 +00:00
StaticPage : pageForReq ( app , r ) ,
OAuthButtons : NewOAuthButtons ( app . Config ( ) ) ,
ForcedLanding : forceLanding ,
2018-11-10 02:14:22 +00:00
}
2019-06-27 21:06:37 +00:00
banner , err := getLandingBanner ( app )
if err != nil {
log . Error ( "unable to get landing banner: %v" , err )
return impart . HTTPError { http . StatusInternalServerError , fmt . Sprintf ( "Could not get banner: %v" , err ) }
}
2019-08-07 13:26:07 +00:00
p . Banner = template . HTML ( applyMarkdown ( [ ] byte ( banner . Content ) , "" , app . cfg ) )
2019-06-27 21:06:37 +00:00
2019-06-28 02:22:21 +00:00
content , err := getLandingBody ( app )
2019-06-27 21:06:37 +00:00
if err != nil {
log . Error ( "unable to get landing content: %v" , err )
return impart . HTTPError { http . StatusInternalServerError , fmt . Sprintf ( "Could not get content: %v" , err ) }
2018-11-10 02:14:22 +00:00
}
2019-08-07 13:26:07 +00:00
p . Content = template . HTML ( applyMarkdown ( [ ] byte ( content . Content ) , "" , app . cfg ) )
2018-11-10 02:14:22 +00:00
// 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 ) )
}
2018-11-08 05:11:42 +00:00
// Show landing page
2018-11-10 02:14:22 +00:00
return renderPage ( w , "landing.tmpl" , p )
2018-11-08 05:11:42 +00:00
}
2019-05-12 20:55:30 +00:00
func handleTemplatedPage ( app * App , w http . ResponseWriter , r * http . Request , t * template . Template ) error {
2018-11-19 02:58:50 +00:00
p := struct {
page . StaticPage
2019-04-11 17:56:07 +00:00
ContentTitle string
2018-11-21 20:04:47 +00:00
Content template . HTML
PlainContent string
Updated string
2018-11-21 19:05:44 +00:00
AboutStats * InstanceStats
2018-11-19 02:58:50 +00:00
} {
StaticPage : pageForReq ( app , r ) ,
}
if r . URL . Path == "/about" || r . URL . Path == "/privacy" {
2019-04-06 17:23:22 +00:00
var c * instanceContent
2018-11-19 02:58:50 +00:00
var err error
if r . URL . Path == "/about" {
c , err = getAboutPage ( app )
2018-11-21 19:05:44 +00:00
// Fetch stats
p . AboutStats = & InstanceStats { }
p . AboutStats . NumPosts , _ = app . db . GetTotalPosts ( )
p . AboutStats . NumBlogs , _ = app . db . GetTotalCollections ( )
2018-11-19 02:58:50 +00:00
} else {
2019-04-06 17:23:22 +00:00
c , err = getPrivacyPage ( app )
2018-11-19 02:58:50 +00:00
}
if err != nil {
return err
}
2019-04-11 17:56:07 +00:00
p . ContentTitle = c . Title . String
2019-08-07 13:26:07 +00:00
p . Content = template . HTML ( applyMarkdown ( [ ] byte ( c . Content ) , "" , app . cfg ) )
2019-04-06 17:23:22 +00:00
p . PlainContent = shortPostDescription ( stripmd . Strip ( c . Content ) )
if ! c . Updated . IsZero ( ) {
p . Updated = c . Updated . Format ( "January 2, 2006" )
2018-11-19 02:58:50 +00:00
}
}
// Serve templated page
err := t . ExecuteTemplate ( w , "base" , p )
if err != nil {
log . Error ( "Unable to render page: %v" , err )
}
return nil
}
2019-05-12 20:55:30 +00:00
func pageForReq ( app * App , r * http . Request ) page . StaticPage {
2018-11-08 04:50:50 +00:00
p := page . StaticPage {
AppCfg : app . cfg . App ,
Path : r . URL . Path ,
2018-11-20 17:14:02 +00:00
Version : "v" + softwareVer ,
2018-11-08 04:50:50 +00:00
}
// 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
2019-08-07 13:00:16 +00:00
p . IsAdmin = u != nil && u . IsAdmin ( )
p . CanInvite = canUserInvite ( app . cfg , p . IsAdmin )
2018-11-08 04:50:50 +00:00
}
}
2019-06-17 00:29:31 +00:00
p . CanViewReader = ! app . cfg . App . Private || u != nil
2018-11-08 04:50:50 +00:00
return p
}
2018-11-08 05:11:42 +00:00
var fileRegex = regexp . MustCompile ( "/([^/]*\\.[^/]*)$" )
2018-10-15 18:44:15 +00:00
2019-06-13 22:50:23 +00:00
// Initialize loads the app configuration and initializes templates, keys,
// session, route handlers, and the database connection.
func Initialize ( apper Apper , debug bool ) ( * App , error ) {
2019-05-10 15:40:35 +00:00
debugging = debug
2018-10-16 20:57:55 +00:00
2019-06-13 22:50:23 +00:00
apper . LoadConfig ( )
2018-11-08 03:13:16 +00:00
2019-06-13 22:50:23 +00:00
// Load templates
err := InitTemplates ( apper . App ( ) . Config ( ) )
2019-01-19 00:17:10 +00:00
if err != nil {
2019-06-13 22:50:23 +00:00
return nil , fmt . Errorf ( "load templates: %s" , err )
2019-01-19 00:17:10 +00:00
}
2018-11-08 04:50:50 +00:00
2019-06-13 22:50:23 +00:00
// Load keys and set up session
initKeyPaths ( apper . App ( ) ) // TODO: find a better way to do this, since it's unneeded in all Apper implementations
err = InitKeys ( apper )
2018-10-15 18:44:15 +00:00
if err != nil {
2019-06-13 22:50:23 +00:00
return nil , fmt . Errorf ( "init keys: %s" , err )
2018-10-17 02:31:27 +00:00
}
2019-08-29 22:05:59 +00:00
apper . App ( ) . InitUpdates ( )
2019-06-13 22:50:23 +00:00
apper . App ( ) . InitSession ( )
2018-10-17 02:31:27 +00:00
2019-06-13 22:50:23 +00:00
apper . App ( ) . InitDecoder ( )
2018-10-17 02:31:27 +00:00
2019-06-13 22:50:23 +00:00
err = ConnectToDatabase ( apper . App ( ) )
2018-12-14 00:15:09 +00:00
if err != nil {
2019-06-13 22:50:23 +00:00
return nil , fmt . Errorf ( "connect to DB: %s" , err )
2018-12-14 00:15:09 +00:00
}
2018-12-10 21:02:42 +00:00
// Handle local timeline, if enabled
2019-06-13 22:50:23 +00:00
if apper . App ( ) . cfg . App . LocalTimeline {
2018-12-10 21:02:42 +00:00
log . Info ( "Initializing local timeline..." )
2019-06-13 22:50:23 +00:00
initLocalTimeline ( apper . App ( ) )
2018-12-10 21:02:42 +00:00
}
2019-06-13 22:50:23 +00:00
return apper . App ( ) , nil
}
func Serve ( app * App , r * mux . Router ) {
log . Info ( "Going to serve..." )
isSingleUser = app . cfg . App . SingleUser
app . cfg . Server . Dev = debugging
2018-10-15 18:44:15 +00:00
// 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 )
} ( )
2020-03-02 01:12:47 +00:00
// Start gopher server
2020-07-23 15:11:11 +00:00
if app . cfg . Server . GopherPort > 0 && ! app . cfg . App . Private {
2020-03-02 01:12:47 +00:00
go initGopher ( app )
}
2018-11-21 23:26:19 +00:00
// Start web application server
2018-11-26 15:50:36 +00:00
var bindAddress = app . cfg . Server . Bind
if bindAddress == "" {
bindAddress = "localhost"
}
2019-06-13 22:50:23 +00:00
var err error
2018-11-21 23:26:19 +00:00
if app . cfg . IsSecureStandalone ( ) {
2019-07-21 00:49:20 +00:00
if app . cfg . Server . Autocert {
m := & autocert . Manager {
2019-07-21 01:34:58 +00:00
Prompt : autocert . AcceptTOS ,
Cache : autocert . DirCache ( app . cfg . Server . TLSCertPath ) ,
}
host , err := url . Parse ( app . cfg . App . Host )
if err != nil {
log . Error ( "[WARNING] Unable to parse configured host! %s" , err )
log . Error ( ` [ WARNING ] ALL hosts are allowed , which can open you to an attack where
clients connect to a server by IP address and pretend to be asking for an
incorrect host name , and cause you to reach the CA ' s rate limit for certificate
requests . We recommend supplying a valid host name . ` )
log . Info ( "Using autocert on ANY host" )
} else {
log . Info ( "Using autocert on host %s" , host . Host )
m . HostPolicy = autocert . HostWhitelist ( host . Host )
2019-07-21 00:49:20 +00:00
}
s := & http . Server {
Addr : ":https" ,
Handler : r ,
TLSConfig : & tls . Config {
GetCertificate : m . GetCertificate ,
} ,
}
s . SetKeepAlivesEnabled ( false )
2019-07-21 01:38:02 +00:00
go func ( ) {
log . Info ( "Serving redirects on http://%s:80" , bindAddress )
err = http . ListenAndServe ( ":80" , m . HTTPHandler ( nil ) )
log . Error ( "Unable to start redirect server: %v" , err )
} ( )
log . Info ( "Serving on https://%s:443" , bindAddress )
2019-07-21 00:49:20 +00:00
log . Info ( "---" )
err = s . ListenAndServeTLS ( "" , "" )
} else {
2019-07-21 01:38:02 +00:00
go func ( ) {
log . Info ( "Serving redirects on http://%s:80" , bindAddress )
err = http . ListenAndServe ( fmt . Sprintf ( "%s:80" , bindAddress ) , 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://%s:443" , bindAddress )
2019-07-21 00:49:20 +00:00
log . Info ( "Using manual certificates" )
log . Info ( "---" )
err = http . ListenAndServeTLS ( fmt . Sprintf ( "%s:443" , bindAddress ) , app . cfg . Server . TLSCertPath , app . cfg . Server . TLSKeyPath , r )
}
2018-11-21 23:26:19 +00:00
} else {
2018-11-26 15:50:36 +00:00
log . Info ( "Serving on http://%s:%d\n" , bindAddress , app . cfg . Server . Port )
2018-11-21 23:26:19 +00:00
log . Info ( "---" )
2019-06-13 22:50:23 +00:00
err = http . ListenAndServe ( fmt . Sprintf ( "%s:%d" , bindAddress , app . cfg . Server . Port ) , r )
2018-11-21 23:26:19 +00:00
}
2018-11-11 01:41:35 +00:00
if err != nil {
log . Error ( "Unable to start: %v" , err )
os . Exit ( 1 )
}
2018-10-15 18:44:15 +00:00
}
2019-06-13 22:50:23 +00:00
func ( app * App ) InitDecoder ( ) {
// TODO: do this at the package level, instead of the App level
// Initialize modules
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 )
}
// 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
}
2019-08-10 16:02:38 +00:00
// FormatVersion constructs the version string for the application
func FormatVersion ( ) string {
return serverSoftware + " " + softwareVer
}
2019-05-10 15:40:35 +00:00
// OutputVersion prints out the version of the application.
func OutputVersion ( ) {
2019-08-10 16:02:38 +00:00
fmt . Println ( FormatVersion ( ) )
2019-05-10 15:40:35 +00:00
}
// NewApp creates a new app instance.
2019-05-12 20:55:30 +00:00
func NewApp ( cfgFile string ) * App {
return & App {
2019-05-10 15:40:35 +00:00
cfgFile : cfgFile ,
}
}
// CreateConfig creates a default configuration and saves it to the app's cfgFile.
2019-05-12 20:55:30 +00:00
func CreateConfig ( app * App ) error {
2019-05-10 15:40:35 +00:00
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.
2019-06-21 09:07:01 +00:00
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 )
2019-05-10 15:40:35 +00:00
if err != nil {
log . Error ( "Unable to configure: %v" , err )
os . Exit ( 1 )
}
2019-06-20 13:04:52 +00:00
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 )
2019-05-10 15:40:35 +00:00
}
2019-06-20 13:04:52 +00:00
} else {
log . Info ( "Database already initialized." )
}
2019-05-10 15:40:35 +00:00
2019-06-20 13:04:52 +00:00
if d . User != nil {
2019-05-10 15:40:35 +00:00
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 )
2019-08-07 20:16:25 +00:00
err = app . db . CreateUser ( app . cfg , u , app . cfg . App . SiteName )
2019-05-10 15:40:35 +00:00
if err != nil {
log . Error ( "Unable to create user: %s" , err )
os . Exit ( 1 )
}
log . Info ( "Done!" )
}
os . Exit ( 0 )
}
2019-06-13 22:50:23 +00:00
// GenerateKeyFiles creates app encryption keys and saves them into the configured KeysParentDir.
func GenerateKeyFiles ( app * App ) error {
2019-05-10 15:40:35 +00:00
// Read keys path from config
2019-06-13 22:50:23 +00:00
app . LoadConfig ( )
2019-05-10 15:40:35 +00:00
// 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 )
2019-06-14 23:11:03 +00:00
// TODO: use something like https://github.com/hashicorp/go-multierror to return errors
2019-05-10 15:40:35 +00:00
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.
2019-06-13 22:50:23 +00:00
func CreateSchema ( apper Apper ) error {
apper . LoadConfig ( )
connectToDatabase ( apper . App ( ) )
defer shutdown ( apper . App ( ) )
err := adminInitDatabase ( apper . App ( ) )
2019-05-10 15:40:35 +00:00
if err != nil {
return err
}
return nil
}
// Migrate runs all necessary database migrations.
2019-07-03 18:39:05 +00:00
func Migrate ( apper Apper ) error {
apper . LoadConfig ( )
connectToDatabase ( apper . App ( ) )
defer shutdown ( apper . App ( ) )
2019-05-10 15:40:35 +00:00
2019-07-03 18:39:05 +00:00
err := migrations . Migrate ( migrations . NewDatastore ( apper . App ( ) . db . DB , apper . App ( ) . db . driverName ) )
2019-05-10 15:40:35 +00:00
if err != nil {
return fmt . Errorf ( "migrate: %s" , err )
}
return nil
}
// ResetPassword runs the interactive password reset process.
2019-07-03 18:39:43 +00:00
func ResetPassword ( apper Apper , username string ) error {
2019-05-10 15:40:35 +00:00
// Connect to the database
2019-07-03 18:39:43 +00:00
apper . LoadConfig ( )
connectToDatabase ( apper . App ( ) )
defer shutdown ( apper . App ( ) )
2019-05-10 15:40:35 +00:00
// Fetch user
2019-07-03 18:39:43 +00:00
u , err := apper . App ( ) . db . GetUserForAuth ( username )
2019-05-10 15:40:35 +00:00
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..." )
2019-07-03 18:39:43 +00:00
err = adminResetPassword ( apper . App ( ) , u , newPass )
2019-05-10 15:40:35 +00:00
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
log . Info ( "Success." )
return nil
}
2019-10-31 22:16:10 +00:00
// DoDeleteAccount runs the confirmation and account delete process.
2019-11-05 17:14:20 +00:00
func DoDeleteAccount ( apper Apper , username string ) error {
2019-10-31 22:16:10 +00:00
// Connect to the database
apper . LoadConfig ( )
connectToDatabase ( apper . App ( ) )
defer shutdown ( apper . App ( ) )
// check user exists
2019-11-05 17:14:20 +00:00
u , err := apper . App ( ) . db . GetUserForAuth ( username )
if err != nil {
2019-10-31 22:16:10 +00:00
log . Error ( "%s" , err )
os . Exit ( 1 )
}
2019-11-05 17:14:20 +00:00
userID := u . ID
// do not delete the admin account
// TODO: check for other admins and skip?
if u . IsAdmin ( ) {
log . Error ( "Can not delete admin account" )
os . Exit ( 1 )
}
2019-10-31 22:16:10 +00:00
// confirm deletion, w/ w/out posts
prompt := promptui . Prompt {
Templates : & promptui . PromptTemplates {
Success : "{{ . | bold | faint }}: " ,
} ,
2019-11-05 17:14:20 +00:00
Label : fmt . Sprintf ( "Really delete user : %s" , username ) ,
2019-10-31 22:16:10 +00:00
IsConfirm : true ,
}
2019-11-05 17:14:20 +00:00
_ , err = prompt . Run ( )
2019-10-31 22:16:10 +00:00
if err != nil {
log . Info ( "Aborted..." )
os . Exit ( 0 )
}
log . Info ( "Deleting..." )
2019-11-05 20:20:07 +00:00
err = apper . App ( ) . db . DeleteAccount ( userID )
2019-10-31 22:16:10 +00:00
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
log . Info ( "Success." )
return nil
}
2019-05-12 20:55:30 +00:00
func connectToDatabase ( app * App ) {
2018-11-21 18:10:10 +00:00
log . Info ( "Connecting to %s database..." , app . cfg . Database . Type )
2018-11-26 20:11:10 +00:00
2018-12-02 22:21:43 +00:00
var db * sql . DB
var err error
2019-01-05 22:51:17 +00:00
if app . cfg . Database . Type == driverMySQL {
2020-04-16 04:04:16 +00:00
db , err = sql . Open ( app . cfg . Database . Type , fmt . Sprintf ( "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&tls=%t" , 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 ( ) ) , app . cfg . Database . TLS ) )
2018-12-03 14:53:31 +00:00
db . SetMaxOpenConns ( 50 )
2019-01-05 22:51:17 +00:00
} else if app . cfg . Database . Type == driverSQLite {
2019-01-03 22:57:06 +00:00
if ! SQLiteEnabled {
log . Error ( "Invalid database type '%s'. Binary wasn't compiled with SQLite3 support." , app . cfg . Database . Type )
os . Exit ( 1 )
}
2018-12-08 18:34:29 +00:00
if app . cfg . Database . FileName == "" {
log . Error ( "SQLite database filename value in config.ini is empty." )
os . Exit ( 1 )
}
2019-01-07 16:55:23 +00:00
db , err = sql . Open ( "sqlite3_with_regex" , app . cfg . Database . FileName + "?parseTime=true&cached=shared" )
2018-12-03 14:53:31 +00:00
db . SetMaxOpenConns ( 1 )
2018-11-26 20:11:10 +00:00
} else {
log . Error ( "Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now." , app . cfg . Database . Type )
2018-11-10 03:16:13 +00:00
os . Exit ( 1 )
}
2018-12-02 22:21:43 +00:00
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
app . db = & datastore { db , app . cfg . Database . Type }
2018-11-10 03:16:13 +00:00
}
2019-05-12 20:55:30 +00:00
func shutdown ( app * App ) {
2018-10-17 02:31:27 +00:00
log . Info ( "Closing database connection..." )
app . db . Close ( )
2018-10-15 18:44:15 +00:00
}
2018-12-17 23:13:26 +00:00
2019-05-12 22:42:57 +00:00
// CreateUser creates a new admin or normal user from the given credentials.
2019-06-13 22:50:23 +00:00
func CreateUser ( apper Apper , username , password string , isAdmin bool ) error {
2018-12-17 23:13:26 +00:00
// Create an admin user with --create-admin
2019-06-13 22:50:23 +00:00
apper . LoadConfig ( )
connectToDatabase ( apper . App ( ) )
defer shutdown ( apper . App ( ) )
2018-12-17 23:13:26 +00:00
// Ensure an admin / first user doesn't already exist
2019-06-13 22:50:23 +00:00
firstUser , _ := apper . App ( ) . db . GetUserByID ( 1 )
2018-12-22 15:54:08 +00:00
if isAdmin {
// Abort if trying to create admin user, but one already exists
if firstUser != nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Admin user already exists (%s). Create a regular user with: writefreely --create-user" , firstUser . Username )
2018-12-22 15:54:08 +00:00
}
} else {
// Abort if trying to create regular user, but no admin exists yet
if firstUser == nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "No admin user exists yet. Create an admin first with: writefreely --create-admin" )
2018-12-22 15:54:08 +00:00
}
2018-12-17 23:13:26 +00:00
}
// Create the user
2018-12-20 02:26:13 +00:00
// Normalize and validate username
desiredUsername := username
username = getSlug ( username , "" )
usernameDesc := username
if username != desiredUsername {
usernameDesc += " (originally: " + desiredUsername + ")"
}
2019-06-13 22:50:23 +00:00
if ! author . IsValidUsername ( apper . App ( ) . cfg , username ) {
return fmt . Errorf ( "Username %s is invalid, reserved, or shorter than configured minimum length (%d characters)." , usernameDesc , apper . App ( ) . cfg . App . MinUsernameLen )
2018-12-20 02:26:13 +00:00
}
// Hash the password
2018-12-17 23:13:26 +00:00
hashedPass , err := auth . HashPass ( [ ] byte ( password ) )
if err != nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Unable to hash password: %v" , err )
2018-12-17 23:13:26 +00:00
}
u := & User {
Username : username ,
HashedPass : hashedPass ,
Created : time . Now ( ) . Truncate ( time . Second ) . UTC ( ) ,
}
2018-12-22 15:54:08 +00:00
userType := "user"
if isAdmin {
userType = "admin"
}
log . Info ( "Creating %s %s..." , userType , usernameDesc )
2019-08-07 20:16:25 +00:00
err = apper . App ( ) . db . CreateUser ( apper . App ( ) . Config ( ) , u , desiredUsername )
2018-12-17 23:13:26 +00:00
if err != nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Unable to create user: %s" , err )
2018-12-17 23:13:26 +00:00
}
log . Info ( "Done!" )
2019-01-26 16:06:58 +00:00
return nil
2018-12-17 23:13:26 +00:00
}
2019-01-13 14:08:47 +00:00
2019-05-12 20:55:30 +00:00
func adminInitDatabase ( app * App ) error {
2019-01-13 14:08:47 +00:00
schemaFileName := "schema.sql"
if app . cfg . Database . Type == driverSQLite {
schemaFileName = "sqlite.sql"
}
schema , err := Asset ( schemaFileName )
if err != nil {
2019-01-26 15:52:11 +00:00
return fmt . Errorf ( "Unable to load schema file: %v" , err )
2019-01-13 14:08:47 +00:00
}
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." )
}
}
2019-01-24 22:08:08 +00:00
// Set up migrations table
2019-04-15 17:48:19 +00:00
log . Info ( "Initializing appmigrations table..." )
2019-01-24 22:08:08 +00:00
err = migrations . SetInitialMigrations ( migrations . NewDatastore ( app . db . DB , app . db . driverName ) )
if err != nil {
2019-01-26 15:52:11 +00:00
return fmt . Errorf ( "Unable to set initial migrations: %v" , err )
2019-01-24 22:08:08 +00:00
}
2019-04-15 17:48:19 +00:00
log . Info ( "Running migrations..." )
err = migrations . Migrate ( migrations . NewDatastore ( app . db . DB , app . db . driverName ) )
if err != nil {
return fmt . Errorf ( "migrate: %s" , err )
}
2019-01-26 15:52:11 +00:00
log . Info ( "Done." )
return nil
2019-01-13 14:08:47 +00:00
}
2020-08-18 16:22:04 +00:00
// ServerUserAgent returns a User-Agent string to use in external requests. The
// hostName parameter may be left empty.
func ServerUserAgent ( hostName string ) string {
hostUAStr := ""
if hostName != "" {
hostUAStr = "; +" + hostName
}
return "Go (" + serverSoftware + "/" + softwareVer + hostUAStr + ")"
}