2020-03-29 08:48:43 +00:00
package main
import (
"database/sql"
"github.com/gorilla/mux"
_ "github.com/mattn/go-sqlite3"
"github.com/rubenv/sql-migrate"
"github.com/spf13/viper"
2020-04-01 17:31:49 +00:00
"html/template"
2020-03-29 08:48:43 +00:00
"log"
"math/rand"
"net/http"
"time"
)
var db * sql . DB
func main ( ) {
rand . Seed ( time . Now ( ) . UTC ( ) . UnixNano ( ) )
viper . SetDefault ( "dbPath" , "data/goshort.db" )
viper . SetConfigName ( "config" )
viper . AddConfigPath ( "./config" )
viper . AddConfigPath ( "." )
_ = viper . ReadInConfig ( )
if ! viper . IsSet ( "dbPath" ) {
log . Fatal ( "No database path (dbPath) is configured." )
}
if ! viper . IsSet ( "password" ) {
log . Fatal ( "No password (password) is configured." )
}
if ! viper . IsSet ( "shortUrl" ) {
log . Fatal ( "No short URL (shortUrl) is configured." )
}
if ! viper . IsSet ( "defaultUrl" ) {
log . Fatal ( "No default URL (defaultUrl) is configured." )
}
var err error
db , err = sql . Open ( "sqlite3" , viper . GetString ( "dbPath" ) )
if err != nil {
log . Fatal ( err )
}
2020-04-02 09:28:35 +00:00
migrateDatabase ( )
2020-03-29 08:48:43 +00:00
defer func ( ) {
_ = db . Close ( )
} ( )
r := mux . NewRouter ( )
2020-04-02 09:28:35 +00:00
admin := r . NewRoute ( ) . Subrouter ( )
admin . HandleFunc ( "/s" , ShortenFormHandler ) . Methods ( http . MethodGet )
admin . HandleFunc ( "/s" , ShortenHandler ) . Methods ( http . MethodPost )
admin . HandleFunc ( "/u" , UpdateFormHandler ) . Methods ( http . MethodGet )
admin . HandleFunc ( "/u" , UpdateHandler ) . Methods ( http . MethodPost )
admin . HandleFunc ( "/d" , DeleteFormHandler ) . Methods ( http . MethodGet )
admin . HandleFunc ( "/d" , DeleteHandler ) . Methods ( http . MethodPost )
admin . HandleFunc ( "/l" , ListHandler ) . Methods ( http . MethodGet )
admin . Use ( loginMiddleware )
2020-03-29 08:48:43 +00:00
r . HandleFunc ( "/{slug}" , ShortenedUrlHandler )
r . HandleFunc ( "/" , CatchAllHandler )
http . Handle ( "/" , r )
log . Fatal ( http . ListenAndServe ( ":8080" , nil ) )
}
2020-04-02 09:28:35 +00:00
func loginMiddleware ( next http . Handler ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
_ = r . ParseForm ( )
if ! checkPassword ( w , r ) {
return
}
next . ServeHTTP ( w , r )
} )
}
func migrateDatabase ( ) {
2020-03-29 08:48:43 +00:00
migrations := & migrate . MemoryMigrationSource {
Migrations : [ ] * migrate . Migration {
{
Id : "001" ,
2020-04-16 08:55:30 +00:00
Up : [ ] string { "create table redirect(slug text not null primary key,url text not null,hits integer default 0 not null);insert into redirect (slug, url) values ('source', 'https://git.jlel.se/jlelse/GoShort');" } ,
2020-03-29 08:48:43 +00:00
Down : [ ] string { "drop table redirect;" } ,
} ,
} ,
}
_ , err := migrate . Exec ( db , "sqlite3" , migrations , migrate . Up )
if err != nil {
log . Fatal ( err )
}
}
2020-04-01 17:31:49 +00:00
func ShortenFormHandler ( w http . ResponseWriter , r * http . Request ) {
2020-04-01 18:00:43 +00:00
err := generateForm ( w , "Shorten URL" , "s" , [ ] [ ] string { { "url" , r . FormValue ( "url" ) } , { "slug" , r . FormValue ( "slug" ) } } )
2020-04-01 17:31:49 +00:00
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
}
}
func UpdateFormHandler ( w http . ResponseWriter , r * http . Request ) {
2020-04-01 18:00:43 +00:00
err := generateForm ( w , "Update short link" , "u" , [ ] [ ] string { { "slug" , r . FormValue ( "slug" ) } , { "new" , r . FormValue ( "new" ) } } )
2020-04-01 17:31:49 +00:00
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
}
}
func DeleteFormHandler ( w http . ResponseWriter , r * http . Request ) {
2020-04-01 18:00:43 +00:00
err := generateForm ( w , "Delete short link" , "d" , [ ] [ ] string { { "slug" , r . FormValue ( "slug" ) } } )
2020-04-01 17:31:49 +00:00
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
}
}
2020-04-01 18:00:43 +00:00
func generateForm ( w http . ResponseWriter , title string , url string , fields [ ] [ ] string ) error {
2020-04-01 18:39:26 +00:00
tmpl , err := template . New ( "Form" ) . Parse ( "<!doctype html><html lang=en><meta name=viewport content=\"width=device-width, initial-scale=1.0\"><title>{{.Title}}</title><h1>{{.Title}}</h1><form action={{.Url}} method=post>{{range .Fields}}<input type=text name={{index . 0}} placeholder={{index . 0}} value=\"{{index . 1}}\"><br><br>{{end}}<input type=submit value={{.Title}}></form></html>" )
2020-04-01 17:31:49 +00:00
if err != nil {
return err
}
err = tmpl . Execute ( w , & struct {
Title string
Url string
2020-04-01 18:00:43 +00:00
Fields [ ] [ ] string
2020-04-01 17:31:49 +00:00
} {
Title : title ,
Url : url ,
Fields : fields ,
} )
if err != nil {
return err
}
return nil
}
2020-03-29 08:48:43 +00:00
func ShortenHandler ( w http . ResponseWriter , r * http . Request ) {
writeShortenedUrl := func ( w http . ResponseWriter , slug string ) {
2020-03-29 17:05:01 +00:00
_ , _ = w . Write ( [ ] byte ( viper . GetString ( "shortUrl" ) + "/" + slug ) )
2020-03-29 08:48:43 +00:00
}
2020-04-01 17:31:49 +00:00
requestUrl := r . FormValue ( "url" )
2020-03-29 08:48:43 +00:00
if requestUrl == "" {
http . Error ( w , "url parameter not set" , http . StatusBadRequest )
return
}
2020-04-01 17:31:49 +00:00
slug := r . FormValue ( "slug" )
2020-03-29 11:32:28 +00:00
manualSlug := false
2020-03-29 09:38:47 +00:00
if slug == "" {
_ = db . QueryRow ( "SELECT slug FROM redirect WHERE url = ?" , requestUrl ) . Scan ( & slug )
2020-03-29 11:32:28 +00:00
} else {
manualSlug = true
2020-03-29 08:48:43 +00:00
}
2020-03-29 09:38:47 +00:00
if slug != "" {
if _ , e := slugExists ( slug ) ; e {
2020-03-29 11:32:28 +00:00
if manualSlug {
http . Error ( w , "slug already in use" , http . StatusBadRequest )
return
}
2020-03-29 09:38:47 +00:00
writeShortenedUrl ( w , slug )
2020-03-29 08:48:43 +00:00
return
}
2020-03-29 09:38:47 +00:00
} else {
var exists = true
for exists == true {
slug = generateSlug ( )
var err error
err , exists = slugExists ( slug )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
}
2020-03-29 08:48:43 +00:00
}
2020-03-29 16:48:05 +00:00
_ , err := db . Exec ( "INSERT INTO redirect (slug, url) VALUES (?, ?)" , slug , requestUrl )
2020-03-29 08:48:43 +00:00
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . WriteHeader ( http . StatusCreated )
writeShortenedUrl ( w , slug )
}
2020-03-30 07:47:21 +00:00
func UpdateHandler ( w http . ResponseWriter , r * http . Request ) {
2020-04-01 17:31:49 +00:00
slug := r . FormValue ( "slug" )
2020-03-30 07:47:21 +00:00
if slug == "" {
http . Error ( w , "Specify the slug to update" , http . StatusBadRequest )
return
}
2020-04-01 17:31:49 +00:00
newUrl := r . FormValue ( "new" )
2020-03-30 07:47:21 +00:00
if newUrl == "" {
http . Error ( w , "Specify the new URL" , http . StatusBadRequest )
return
}
if err , e := slugExists ( slug ) ; ! e || err != nil {
http . Error ( w , "Slug not found" , http . StatusNotFound )
return
}
_ , err := db . Exec ( "UPDATE redirect SET url = ? WHERE slug = ?" , newUrl , slug )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . WriteHeader ( http . StatusAccepted )
_ , _ = w . Write ( [ ] byte ( "Slug updated" ) )
}
2020-03-29 10:47:35 +00:00
func DeleteHandler ( w http . ResponseWriter , r * http . Request ) {
2020-04-01 17:31:49 +00:00
slug := r . FormValue ( "slug" )
2020-03-29 10:47:35 +00:00
if slug == "" {
http . Error ( w , "Specify the slug to delete" , http . StatusBadRequest )
return
}
if err , e := slugExists ( slug ) ; ! e || err != nil {
http . Error ( w , "Slug not found" , http . StatusNotFound )
return
}
2020-03-29 16:48:05 +00:00
_ , err := db . Exec ( "DELETE FROM redirect WHERE slug = ?" , slug )
2020-03-29 10:47:35 +00:00
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
w . WriteHeader ( http . StatusAccepted )
_ , _ = w . Write ( [ ] byte ( "Slug deleted" ) )
}
2020-04-02 09:28:35 +00:00
func ListHandler ( w http . ResponseWriter , r * http . Request ) {
type row struct {
Slug string
Url string
Hits int
}
var list [ ] row
rows , err := db . Query ( "SELECT slug, url, hits FROM redirect" )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
for rows . Next ( ) {
var r row
err = rows . Scan ( & r . Slug , & r . Url , & r . Hits )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
list = append ( list , r )
}
tmpl , err := template . New ( "List" ) . Parse ( "<!doctype html><html lang=en><meta name=viewport content=\"width=device-width, initial-scale=1.0\"><title>Short URLs</title><h1>Short URLs</h1><table><tr><th>slug</th><th>url</th><th>hits</th></tr>{{range .}}<tr><td>{{.Slug}}</td><td>{{.Url}}</td><td>{{.Hits}}</td></tr>{{end}}</table></html>" )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
err = tmpl . Execute ( w , & list )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
}
2020-03-29 11:08:47 +00:00
func checkPassword ( w http . ResponseWriter , r * http . Request ) bool {
2020-04-01 17:31:49 +00:00
if r . FormValue ( "password" ) == viper . GetString ( "password" ) {
2020-03-29 11:08:47 +00:00
return true
}
_ , pass , ok := r . BasicAuth ( )
if ! ( ok && pass == viper . GetString ( "password" ) ) {
w . Header ( ) . Set ( "WWW-Authenticate" , ` Basic realm="Please enter a password!" ` )
http . Error ( w , "Not authenticated" , http . StatusUnauthorized )
return false
}
return true
}
2020-03-29 08:48:43 +00:00
func generateSlug ( ) string {
var chars = [ ] rune ( "0123456789abcdefghijklmnopqrstuvwxyz" )
s := make ( [ ] rune , 6 )
for i := range s {
s [ i ] = chars [ rand . Intn ( len ( chars ) ) ]
}
return string ( s )
}
func slugExists ( slug string ) ( e error , exists bool ) {
err := db . QueryRow ( "SELECT EXISTS(SELECT * FROM redirect WHERE slug = ?)" , slug ) . Scan ( & exists )
if err != nil {
return err , false
}
return nil , exists
}
func ShortenedUrlHandler ( w http . ResponseWriter , r * http . Request ) {
slug , ok := mux . Vars ( r ) [ "slug" ]
if ! ok {
CatchAllHandler ( w , r )
return
}
var redirectUrl string
err := db . QueryRow ( "SELECT url FROM redirect WHERE slug = ?" , slug ) . Scan ( & redirectUrl )
if err != nil {
http . NotFound ( w , r )
return
}
2020-03-29 16:36:44 +00:00
go func ( ) {
2020-03-29 16:48:05 +00:00
_ , _ = db . Exec ( "UPDATE redirect SET hits = hits + 1 WHERE slug = ?" , slug )
2020-03-29 16:36:44 +00:00
} ( )
2020-03-29 08:48:43 +00:00
http . Redirect ( w , r , redirectUrl , http . StatusTemporaryRedirect )
}
func CatchAllHandler ( w http . ResponseWriter , r * http . Request ) {
http . Redirect ( w , r , viper . GetString ( "defaultUrl" ) , http . StatusTemporaryRedirect )
}