diff --git a/assets/templates/html/index.html.tmpl b/assets/templates/html/index.html.tmpl index f5520f5..7f6f45b 100644 --- a/assets/templates/html/index.html.tmpl +++ b/assets/templates/html/index.html.tmpl @@ -9,10 +9,10 @@ the command line. ## USAGE # Upload a file - curl -F'file=@yourfile.png' https://{{.Request.Host}} + curl -F'file=@yourfile.png' {{.Host}} # 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. # @@ -20,6 +20,6 @@ the command line. # information on how to delete the shortened object. # 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 {{end}} diff --git a/assets/templates/html/newFileUploadPasteSuccess.html.tmpl b/assets/templates/html/newFileUploadPasteSuccess.html.tmpl index 2879864..d5f8f3b 100644 --- a/assets/templates/html/newFileUploadPasteSuccess.html.tmpl +++ b/assets/templates/html/newFileUploadPasteSuccess.html.tmpl @@ -4,13 +4,13 @@ Success - rushlink {{define "body"}}
-https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}} +{{.Host}}/{{.Paste.Key}}{{.FileExt}} --- # 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 - curl --request DELETE https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}} + curl --request DELETE {{.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}}{{end}} \ No newline at end of file diff --git a/assets/templates/html/newRedirectPasteSuccess.html.tmpl b/assets/templates/html/newRedirectPasteSuccess.html.tmpl index 2b8f980..b1b0372 100644 --- a/assets/templates/html/newRedirectPasteSuccess.html.tmpl +++ b/assets/templates/html/newRedirectPasteSuccess.html.tmpl @@ -4,13 +4,13 @@ Success - rushlink {{define "body"}}
-https://{{.Request.Host}}/{{.Paste.Key}} +{{.Host}}/{{.Paste.Key}} --- # 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 -curl --request DELETE https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} +curl --request DELETE {{.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}{{end}} \ No newline at end of file diff --git a/assets/templates/txt/deletePasteSuccess.txt.tmpl b/assets/templates/txt/deletePasteSuccess.txt.tmpl index 0ffae3c..ac40c43 100644 --- a/assets/templates/txt/deletePasteSuccess.txt.tmpl +++ b/assets/templates/txt/deletePasteSuccess.txt.tmpl @@ -1 +1 @@ -<{{.Request.Host}}/{{.Paste.Key}}> was succesfully deleted +<{{.Host}}/{{.Paste.Key}}> was succesfully deleted diff --git a/assets/templates/txt/index.txt.tmpl b/assets/templates/txt/index.txt.tmpl index 5a02660..0dd996d 100644 --- a/assets/templates/txt/index.txt.tmpl +++ b/assets/templates/txt/index.txt.tmpl @@ -7,10 +7,10 @@ the command line. ## USAGE # Upload a file - curl -F'file=@yourfile.png' https://{{.Request.Host}} + curl -F'file=@yourfile.png' {{.Host}} # 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. # @@ -18,4 +18,4 @@ the command line. # information on how to delete the shortened object. # 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 \ No newline at end of file + curl -F'file=@yourfile.png' {{.Host}} | head -n 1 \ No newline at end of file diff --git a/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl b/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl index 7100139..a94e8cd 100644 --- a/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl +++ b/assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl @@ -1,8 +1,8 @@ -https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}} +{{.Host}}/{{.Paste.Key}}{{.FileExt}} --- # 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 -curl --request DELETE https://{{.Request.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}} +curl --request DELETE {{.Host}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken | urlquery}} diff --git a/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl b/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl index 96ebba8..5b1e2f6 100644 --- a/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl +++ b/assets/templates/txt/newRedirectPasteSuccess.txt.tmpl @@ -1,8 +1,8 @@ -https://{{.Request.Host}}/{{.Paste.Key}} +{{.Host}}/{{.Paste.Key}} --- # 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 -curl --request DELETE https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} +curl --request DELETE {{.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} diff --git a/assets/templates/txt/pasteMeta.txt.tmpl b/assets/templates/txt/pasteMeta.txt.tmpl index 51e0719..0eb2ceb 100644 --- a/assets/templates/txt/pasteMeta.txt.tmpl +++ b/assets/templates/txt/pasteMeta.txt.tmpl @@ -1,4 +1,4 @@ -METADATA on <{{.Request.Host}}/{{.Paste.Key}}>: +METADATA on <{{.Host}}/{{.Paste.Key}}>: TYPE: {{.Paste.Type}} STATE: {{.Paste.State}} @@ -12,6 +12,6 @@ DELETE TOKEN: {{.CanDelete.String}} {{if and (ne .Paste.State.String "deleted") .CanDelete.Bool}} ``` # 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}} diff --git a/cmd/rushlink/main.go b/cmd/rushlink/main.go index 98cad7a..f5ea838 100644 --- a/cmd/rushlink/main.go +++ b/cmd/rushlink/main.go @@ -13,6 +13,7 @@ var ( 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)") 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() { @@ -29,5 +30,5 @@ func main() { defer database.Close() go rushlink.StartMetricsServer(*metricsListen, database, filestore) - rushlink.StartMainServer(*httpListen, database, filestore) + rushlink.StartMainServer(*httpListen, database, filestore, hostURL) } diff --git a/handlers.go b/handlers.go index 06117ab..20b7c77 100644 --- a/handlers.go +++ b/handlers.go @@ -30,7 +30,7 @@ const ( const cookieDeleteToken = "owner_token" 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) { @@ -49,7 +49,7 @@ func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) return err }); err != nil { if badID { - renderError(w, r, http.StatusNotFound, "malformed file id") + rl.renderError(w, r, http.StatusNotFound, "malformed file id") return } // unexpected error @@ -61,7 +61,7 @@ func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) if err != nil { if os.IsNotExist(err) { 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 } // unexpected error @@ -114,7 +114,7 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request } 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 } @@ -139,7 +139,7 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request "Paste": p, "CanDelete": canDelete, } - render(w, r, "pasteMeta", data) + rl.render(w, r, "pasteMeta", data) return } @@ -164,7 +164,7 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request } fmt.Fprint(w, location) 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: 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 } else { msg := fmt.Sprintf("could not parse form: %v\n", err) - renderError(w, r, http.StatusBadRequest, msg) + rl.renderError(w, r, http.StatusBadRequest, msg) return } @@ -189,7 +189,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) { 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) { @@ -214,7 +214,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req "Paste": paste, "FileUpload": fu, "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) { @@ -224,7 +224,7 @@ func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Req } shorten := r.PostFormValue("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 } rl.newRedirectPasteHandler(w, r, shorten) @@ -234,15 +234,15 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque userURL, err := url.ParseRequestURI(rawurl) if err != nil { msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) - renderError(w, r, http.StatusBadRequest, msg) + rl.renderError(w, r, http.StatusBadRequest, msg) return } 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 } 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 } @@ -255,7 +255,7 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque panic(err) } data := map[string]interface{}{"Paste": paste} - render(w, r, "newRedirectPasteSuccess", data) + rl.render(w, r, "newRedirectPasteSuccess", data) } // Delete a URL from the database @@ -265,7 +265,7 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) { deleteToken := getDeleteTokenFromRequest(r) if deleteToken == "" { - renderError(w, r, http.StatusBadRequest, "no delete token provided\n") + rl.renderError(w, r, http.StatusBadRequest, "no delete token provided\n") return } @@ -293,12 +293,12 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) { return nil }); err != nil { 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 } 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 diff --git a/router.go b/router.go index b18907f..33ef50f 100644 --- a/router.go +++ b/router.go @@ -4,23 +4,30 @@ import ( "fmt" "log" "net/http" + "net/url" "runtime/debug" "strconv" "time" "gitea.hashru.nl/dsprenkels/rushlink/internal/db" "github.com/gorilla/mux" + "github.com/pkg/errors" ) const urlKeyExpr = "{key:[A-Za-z0-9-_]{4,}}" const urlKeyWithExtExpr = urlKeyExpr + "{ext:\\.[A-Za-z0-9-_]+}" type rushlink struct { - db *db.Database - fs *db.FileStore + db *db.Database + 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) { defer func() { defer func() { @@ -34,13 +41,22 @@ func recoveryMiddleware(next http.Handler) http.Handler { if err := recover(); err != nil { log.Printf("error: %v\n", err) debug.PrintStack() - renderInternalServerError(w, r, err) + rl.renderInternalServerError(w, r, err) } }() 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 { Inner http.ResponseWriter StatusCode int @@ -62,26 +78,26 @@ func (w *statusResponseWriter) WriteHeader(statusCode int) { 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. -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{ - db: db, - fs: fs, + db: db, + fs: fs, + host: host, } // Initialize Gorilla router router := mux.NewRouter() - router.Use(recoveryMiddleware) - router.Use(metricsMiddleware) + router.Use(rl.recoveryMiddleware) + router.Use(rl.metricsMiddleware) router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", rl.uploadFileGetHandler).Methods("GET", "HEAD") router.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD") router.HandleFunc("/", rl.newPasteHandler).Methods("POST") diff --git a/views.go b/views.go index 2c2a305..be7f9b5 100644 --- a/views.go +++ b/views.go @@ -76,7 +76,22 @@ func parseFail(tmplName string, err error) { 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"}) if err != nil { 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 - data["Request"] = r + mapExtend(data, "Host", rl.resolveHost(r)) + mapExtend(data, "Request", r) switch contentType { case "text/plain": @@ -117,10 +133,9 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st } w.WriteHeader(http.StatusOK) - if r.Method == "HEAD" { - return + if r.Method != "HEAD" { + err = tmpl.Execute(w, data) } - err = tmpl.Execute(w, data) default: // Fall back to plain text without template 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) - 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) - 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.