Implement --host flag to override {{.Host}}

Fixes #15
This commit is contained in:
Daan Sprenkels 2019-12-15 16:48:50 +05:30
parent 41f4de43ac
commit 0bffde1dc1
12 changed files with 98 additions and 66 deletions

View File

@ -9,10 +9,10 @@ the command line.
## USAGE ## USAGE
# Upload a file # Upload a file
curl -F'file=@yourfile.png' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> curl -F'file=@yourfile.png' <a href="{{.Host}}">{{.Host}}</a>
# Shorten a URL # Shorten a URL
curl -F'shorten=http://example.com/some/long/url' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> curl -F'shorten=http://example.com/some/long/url' <a href="{{.Host}}">{{.Host}}</a>
# The first line of the result will contain the shortened URL. # The first line of the result will contain the shortened URL.
# #
@ -20,6 +20,6 @@ the command line.
# information on how to delete the shortened object. # information on how to delete the shortened object.
# To upload a file and only extract the shortened URL (i.e. throw away the rest) # To upload a file and only extract the shortened URL (i.e. throw away the rest)
curl -F'file=@yourfile.png' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> | head -n 1 curl -F'file=@yourfile.png' <a href="{{.Host}}">{{.Host}}</a> | head -n 1
</pre> </pre>
{{end}} {{end}}

View File

@ -4,13 +4,13 @@ Success - rushlink
{{define "body"}} {{define "body"}}
<pre> <pre>
https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}} {{.Host}}/{{.Paste.Key}}{{.FileExt}}
--- ---
# View metadata # View metadata
curl <a href="https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}">https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}</a> curl <a href="{{.Host}}/{{.Paste.Key}}{{.FileExt}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}">{{.Host}}/{{.Paste.Key}}{{.FileExt}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}</a>
# Delete this object # Delete this object
curl --request DELETE <a href="https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}}">https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}}</a> curl --request DELETE <a href="{{.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}}">{{.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}}</a>
</pre> </pre>
{{end}} {{end}}

View File

@ -4,13 +4,13 @@ Success - rushlink
{{define "body"}} {{define "body"}}
<pre> <pre>
https://{{.Request.Host}}/{{.Paste.Key}} {{.Host}}/{{.Paste.Key}}
--- ---
# View metadata # View metadata
curl <a href="https://{{.Request.Host}}/{{.Paste.Key}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}">https://{{.Request.Host}}/{{.Paste.Key}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}</a> curl <a href="{{.Host}}/{{.Paste.Key}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}">{{.Host}}/{{.Paste.Key}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}</a>
# Delete this object # Delete this object
curl --request DELETE <a href="https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}">https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}</a> curl --request DELETE <a href="{{.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}">{{.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}</a>
</pre> </pre>
{{end}} {{end}}

View File

@ -1 +1 @@
<{{.Request.Host}}/{{.Paste.Key}}> was succesfully deleted <{{.Host}}/{{.Paste.Key}}> was succesfully deleted

View File

@ -7,10 +7,10 @@ the command line.
## USAGE ## USAGE
# Upload a file # Upload a file
curl -F'file=@yourfile.png' https://{{.Request.Host}} curl -F'file=@yourfile.png' {{.Host}}
# Shorten a URL # Shorten a URL
curl -F'shorten=http://example.com/some/long/url' https://{{.Request.Host}} curl -F'shorten=http://example.com/some/long/url' {{.Host}}
# The first line of the result will contain the shortened URL. # The first line of the result will contain the shortened URL.
# #
@ -18,4 +18,4 @@ the command line.
# information on how to delete the shortened object. # information on how to delete the shortened object.
# To upload a file and only extract the shortened URL (i.e. throw away the rest) # To upload a file and only extract the shortened URL (i.e. throw away the rest)
curl -F'file=@yourfile.png' https://{{.Request.Host}} | head -n 1 curl -F'file=@yourfile.png' {{.Host}} | head -n 1

View File

@ -1,8 +1,8 @@
https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}} {{.Host}}/{{.Paste.Key}}{{.FileExt}}
--- ---
# View metadata # View metadata
curl https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}} curl {{.Host}}/{{.Paste.Key}}{{.FileExt}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}
# Delete this object # Delete this object
curl --request DELETE https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}} curl --request DELETE {{.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}}

View File

@ -1,8 +1,8 @@
https://{{.Request.Host}}/{{.Paste.Key}} {{.Host}}/{{.Paste.Key}}
--- ---
# View metadata # View metadata
curl https://{{.Request.Host}}/{{.Paste.Key}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}} curl {{.Host}}/{{.Paste.Key}}/meta?deleteToken={{.Paste.DeleteToken | urlquery}}
# Delete this object # Delete this object
curl --request DELETE https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} curl --request DELETE {{.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}

View File

@ -1,4 +1,4 @@
METADATA on <{{.Request.Host}}/{{.Paste.Key}}>: METADATA on <{{.Host}}/{{.Paste.Key}}>:
TYPE: {{.Paste.Type}} TYPE: {{.Paste.Type}}
STATE: {{.Paste.State}} STATE: {{.Paste.State}}
@ -12,6 +12,6 @@ DELETE TOKEN: {{.CanDelete.String}}
{{if and (ne .Paste.State.String "deleted") .CanDelete.Bool}} {{if and (ne .Paste.State.String "deleted") .CanDelete.Bool}}
``` ```
# To delete this {{.Paste.Type}}, execute: # To delete this {{.Paste.Type}}, execute:
curl --request "DELETE" "{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Request.URL.Query.Get "deleteToken"}}" curl --request "DELETE" "{{.Host}}/{{.Paste.Key}}?deleteToken={{.Request.URL.Query.Get "deleteToken"}}"
``` ```
{{end}} {{end}}

View File

@ -13,6 +13,7 @@ var (
fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored") fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored")
httpListen = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)") httpListen = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)")
metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)") metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)")
hostURL = flag.String("host", "", "host root (defaults to using the request 'Host' header with HTTPS")
) )
func main() { func main() {
@ -29,5 +30,5 @@ func main() {
defer database.Close() defer database.Close()
go rushlink.StartMetricsServer(*metricsListen, database, filestore) go rushlink.StartMetricsServer(*metricsListen, database, filestore)
rushlink.StartMainServer(*httpListen, database, filestore) rushlink.StartMainServer(*httpListen, database, filestore, hostURL)
} }

View File

@ -30,7 +30,7 @@ const (
const cookieDeleteToken = "owner_token" const cookieDeleteToken = "owner_token"
func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) {
render(w, r, "index", map[string]interface{}{}) rl.render(w, r, "index", map[string]interface{}{})
} }
func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) {
@ -49,7 +49,7 @@ func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request)
return err return err
}); err != nil { }); err != nil {
if badID { if badID {
renderError(w, r, http.StatusNotFound, "malformed file id") rl.renderError(w, r, http.StatusNotFound, "malformed file id")
return return
} }
// unexpected error // unexpected error
@ -61,7 +61,7 @@ func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath) log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath)
renderError(w, r, http.StatusNotFound, "file not found") rl.renderError(w, r, http.StatusNotFound, "file not found")
return return
} }
// unexpected error // unexpected error
@ -114,7 +114,7 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
} }
if p == nil { if p == nil {
renderError(w, r, http.StatusNotFound, "url key not found in the database") rl.renderError(w, r, http.StatusNotFound, "url key not found in the database")
return return
} }
@ -139,7 +139,7 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
"Paste": p, "Paste": p,
"CanDelete": canDelete, "CanDelete": canDelete,
} }
render(w, r, "pasteMeta", data) rl.render(w, r, "pasteMeta", data)
return return
} }
@ -164,7 +164,7 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
} }
fmt.Fprint(w, location) fmt.Fprint(w, location)
case db.PasteStateDeleted: case db.PasteStateDeleted:
renderError(w, r, http.StatusGone, "paste has been deleted\n") rl.renderError(w, r, http.StatusGone, "paste has been deleted\n")
default: default:
panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key)) panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key))
} }
@ -179,7 +179,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
// Fallthrough // Fallthrough
} else { } else {
msg := fmt.Sprintf("could not parse form: %v\n", err) msg := fmt.Sprintf("could not parse form: %v\n", err)
renderError(w, r, http.StatusBadRequest, msg) rl.renderError(w, r, http.StatusBadRequest, msg)
return return
} }
@ -189,7 +189,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n") rl.renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n")
} }
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) { func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
@ -214,7 +214,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
"Paste": paste, "Paste": paste,
"FileUpload": fu, "FileUpload": fu,
"FileExt": filepath.Ext(fu.FileName)} "FileExt": filepath.Ext(fu.FileName)}
render(w, r, "newFileUploadPasteSuccess", data) rl.render(w, r, "newFileUploadPasteSuccess", data)
} }
func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
@ -224,7 +224,7 @@ func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Req
} }
shorten := r.PostFormValue("shorten") shorten := r.PostFormValue("shorten")
if shorten == "" { if shorten == "" {
renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n") rl.renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n")
return return
} }
rl.newRedirectPasteHandler(w, r, shorten) rl.newRedirectPasteHandler(w, r, shorten)
@ -234,15 +234,15 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque
userURL, err := url.ParseRequestURI(rawurl) userURL, err := url.ParseRequestURI(rawurl)
if err != nil { if err != nil {
msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
renderError(w, r, http.StatusBadRequest, msg) rl.renderError(w, r, http.StatusBadRequest, msg)
return return
} }
if userURL.Scheme == "" { if userURL.Scheme == "" {
renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n") rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n")
return return
} }
if userURL.Host == "" { if userURL.Host == "" {
renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n") rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n")
return return
} }
@ -255,7 +255,7 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque
panic(err) panic(err)
} }
data := map[string]interface{}{"Paste": paste} data := map[string]interface{}{"Paste": paste}
render(w, r, "newRedirectPasteSuccess", data) rl.render(w, r, "newRedirectPasteSuccess", data)
} }
// Delete a URL from the database // Delete a URL from the database
@ -265,7 +265,7 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
deleteToken := getDeleteTokenFromRequest(r) deleteToken := getDeleteTokenFromRequest(r)
if deleteToken == "" { if deleteToken == "" {
renderError(w, r, http.StatusBadRequest, "no delete token provided\n") rl.renderError(w, r, http.StatusBadRequest, "no delete token provided\n")
return return
} }
@ -293,12 +293,12 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
return nil return nil
}); err != nil { }); err != nil {
log.Printf("error: %v\n", err) log.Printf("error: %v\n", err)
renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err)) rl.renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err))
return return
} }
data := map[string]interface{}{"Paste": paste} data := map[string]interface{}{"Paste": paste}
render(w, r, "deletePasteSuccess", data) rl.render(w, r, "deletePasteSuccess", data)
} }
// Add a new fileUpload redirect to the database // Add a new fileUpload redirect to the database

View File

@ -4,23 +4,30 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"time" "time"
"gitea.hashru.nl/dsprenkels/rushlink/internal/db" "gitea.hashru.nl/dsprenkels/rushlink/internal/db"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors"
) )
const urlKeyExpr = "{key:[A-Za-z0-9-_]{4,}}" const urlKeyExpr = "{key:[A-Za-z0-9-_]{4,}}"
const urlKeyWithExtExpr = urlKeyExpr + "{ext:\\.[A-Za-z0-9-_]+}" const urlKeyWithExtExpr = urlKeyExpr + "{ext:\\.[A-Za-z0-9-_]+}"
type rushlink struct { type rushlink struct {
db *db.Database db *db.Database
fs *db.FileStore fs *db.FileStore
host *url.URL
} }
func recoveryMiddleware(next http.Handler) http.Handler { func (rl *rushlink) Host() *url.URL {
return rl.host
}
func (rl *rushlink) recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() { defer func() {
defer func() { defer func() {
@ -34,13 +41,22 @@ func recoveryMiddleware(next http.Handler) http.Handler {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Printf("error: %v\n", err) log.Printf("error: %v\n", err)
debug.PrintStack() debug.PrintStack()
renderInternalServerError(w, r, err) rl.renderInternalServerError(w, r, err)
} }
}() }()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
func (rl *rushlink) metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
srw := statusResponseWriter{Inner: w}
next.ServeHTTP(&srw, r)
status := strconv.Itoa(srw.StatusCode)
metricRequestsTotalCounter.WithLabelValues(status, r.Method).Inc()
})
}
type statusResponseWriter struct { type statusResponseWriter struct {
Inner http.ResponseWriter Inner http.ResponseWriter
StatusCode int StatusCode int
@ -62,26 +78,26 @@ func (w *statusResponseWriter) WriteHeader(statusCode int) {
w.Inner.WriteHeader(statusCode) w.Inner.WriteHeader(statusCode)
} }
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
srw := statusResponseWriter{Inner: w}
next.ServeHTTP(&srw, r)
status := strconv.Itoa(srw.StatusCode)
metricRequestsTotalCounter.WithLabelValues(status, r.Method).Inc()
})
}
// StartMainServer starts the main http server listening on addr. // StartMainServer starts the main http server listening on addr.
func StartMainServer(addr string, db *db.Database, fs *db.FileStore) { func StartMainServer(addr string, db *db.Database, fs *db.FileStore, rawhost *string) {
var host *url.URL
if rawhost != nil {
var err error
host, err = url.Parse(*rawhost)
if err != nil {
log.Fatalln(errors.Wrap(err, "could not parse host flag"))
}
}
rl := rushlink{ rl := rushlink{
db: db, db: db,
fs: fs, fs: fs,
host: host,
} }
// Initialize Gorilla router // Initialize Gorilla router
router := mux.NewRouter() router := mux.NewRouter()
router.Use(recoveryMiddleware) router.Use(rl.recoveryMiddleware)
router.Use(metricsMiddleware) router.Use(rl.metricsMiddleware)
router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", rl.uploadFileGetHandler).Methods("GET", "HEAD") router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", rl.uploadFileGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD") router.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/", rl.newPasteHandler).Methods("POST") router.HandleFunc("/", rl.newPasteHandler).Methods("POST")

View File

@ -76,7 +76,22 @@ func parseFail(tmplName string, err error) {
panic(errors.Wrapf(err, "parsing of %v failed", tmplName)) panic(errors.Wrapf(err, "parsing of %v failed", tmplName))
} }
func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[string]interface{}) { func mapExtend(m map[string]interface{}, key string, value interface{}) {
if m[key] != nil {
return
}
m[key] = value
}
func (rl *rushlink) resolveHost(r *http.Request) string {
rlHost := rl.Host()
if rlHost != nil {
return rlHost.String()
}
return r.Host
}
func (rl *rushlink) render(w http.ResponseWriter, r *http.Request, tmplName string, data map[string]interface{}) {
contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"}) contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"})
if err != nil { if err != nil {
w.WriteHeader(http.StatusNotAcceptable) w.WriteHeader(http.StatusNotAcceptable)
@ -84,7 +99,8 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
} }
// Add the request to the template data // Add the request to the template data
data["Request"] = r mapExtend(data, "Host", rl.resolveHost(r))
mapExtend(data, "Request", r)
switch contentType { switch contentType {
case "text/plain": case "text/plain":
@ -117,10 +133,9 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
} }
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if r.Method == "HEAD" { if r.Method != "HEAD" {
return err = tmpl.Execute(w, data)
} }
err = tmpl.Execute(w, data)
default: default:
// Fall back to plain text without template // Fall back to plain text without template
w.WriteHeader(http.StatusNotAcceptable) w.WriteHeader(http.StatusNotAcceptable)
@ -132,14 +147,14 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
} }
} }
func renderError(w http.ResponseWriter, r *http.Request, status int, msg string) { func (rl *rushlink) renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
w.WriteHeader(status) w.WriteHeader(status)
render(w, r, "error", map[string]interface{}{"Message": msg}) rl.render(w, r, "error", map[string]interface{}{"Message": msg})
} }
func renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) { func (rl *rushlink) renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
msg := fmt.Sprintf("internal server error: %v", err) msg := fmt.Sprintf("internal server error: %v", err)
renderError(w, r, http.StatusInternalServerError, msg) rl.renderError(w, r, http.StatusInternalServerError, msg)
} }
// Try to resolve the preferred content-type for the response to this request. // Try to resolve the preferred content-type for the response to this request.