URL shortener and file dump for hashru.link
https://hashru.link
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
149 lines
4.6 KiB
149 lines
4.6 KiB
package rushlink |
|
|
|
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 staticFilenameExpr = "[A-Za-z0-9-_.]+" |
|
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 |
|
rootURL *url.URL |
|
} |
|
|
|
func (rl *rushlink) RootURL() *url.URL { |
|
return rl.rootURL |
|
} |
|
|
|
func (rl *rushlink) recoveryMiddleware(next http.Handler) http.Handler { |
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
logRequestInfo := func() { |
|
log.Printf("in request: %v - %v %q %v", r.RemoteAddr, r.Method, r.RequestURI, r.Proto) |
|
} |
|
defer func() { |
|
defer func() { |
|
if err := recover(); err != nil { |
|
w.WriteHeader(500) |
|
log.Printf("error: panic while recovering from another panic: %v\n", err) |
|
logRequestInfo() |
|
debug.PrintStack() |
|
fmt.Fprintf(w, "internal server error: %v\n", err) |
|
} |
|
}() |
|
|
|
if err := recover(); err != nil { |
|
w.WriteHeader(500) |
|
log.Printf("error: %v\n", err) |
|
logRequestInfo() |
|
debug.PrintStack() |
|
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) { |
|
tick := time.Now() |
|
srw := statusResponseWriter{Inner: w} |
|
next.ServeHTTP(&srw, r) |
|
tock := time.Now() |
|
|
|
status := strconv.Itoa(srw.StatusCode) |
|
labels := map[string]string{"code": status, "method": r.Method} |
|
// Update requests counter metric |
|
metricRequestsTotalCounter.With(labels).Inc() |
|
// Update request latency metric |
|
elapsed := tock.Sub(tick) |
|
metricRequestsLatencyNanoSeconds.With(labels).Observe(elapsed.Seconds()) |
|
}) |
|
} |
|
|
|
type statusResponseWriter struct { |
|
Inner http.ResponseWriter |
|
StatusCode int |
|
} |
|
|
|
func (w *statusResponseWriter) Header() http.Header { |
|
return w.Inner.Header() |
|
} |
|
|
|
func (w *statusResponseWriter) Write(buf []byte) (int, error) { |
|
if w.StatusCode == 0 { |
|
w.WriteHeader(http.StatusOK) |
|
} |
|
return w.Inner.Write(buf) |
|
} |
|
|
|
func (w *statusResponseWriter) WriteHeader(statusCode int) { |
|
w.StatusCode = statusCode |
|
w.Inner.WriteHeader(statusCode) |
|
} |
|
|
|
// InitMainRouter creates the main Gorilla router for the application. |
|
// |
|
// This function will not populate the router with an error-recovery and |
|
// metrics-reporting middleware. If these middleware are required, then the |
|
// caller should encapsulate this router inside of another router and register |
|
// the middlewares on the encapsulating router. |
|
func InitMainRouter(r *mux.Router, rl *rushlink) { |
|
r.HandleFunc("/{path:img/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD") |
|
r.HandleFunc("/{path:css/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD") |
|
r.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD") |
|
r.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD") |
|
r.HandleFunc("/", rl.newPasteHandler).Methods("POST") |
|
r.HandleFunc("/"+urlKeyExpr, rl.viewPasteHandler).Methods("GET", "HEAD") |
|
r.HandleFunc("/"+urlKeyWithExtExpr, rl.viewPasteHandler).Methods("GET", "HEAD") |
|
r.HandleFunc("/"+urlKeyExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD") |
|
r.HandleFunc("/"+urlKeyWithExtExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD") |
|
r.HandleFunc("/"+urlKeyExpr+"/meta", rl.viewPasteHandlerMeta).Methods("GET", "HEAD") |
|
r.HandleFunc("/"+urlKeyWithExtExpr+"/meta", rl.viewPasteHandlerMeta).Methods("GET", "HEAD") |
|
r.HandleFunc("/"+urlKeyExpr, rl.deletePasteHandler).Methods("DELETE") |
|
r.HandleFunc("/"+urlKeyWithExtExpr, rl.deletePasteHandler).Methods("DELETE") |
|
r.HandleFunc("/"+urlKeyExpr+"/delete", rl.deletePasteHandler).Methods("POST") |
|
r.HandleFunc("/"+urlKeyWithExtExpr+"/delete", rl.deletePasteHandler).Methods("POST") |
|
} |
|
|
|
// StartMainServer starts the main http server listening on addr. |
|
func StartMainServer(addr string, db *db.Database, fs *db.FileStore, rawRootURL string) { |
|
var rootURL *url.URL |
|
if rawRootURL != "" { |
|
var err error |
|
rootURL, err = url.Parse(rawRootURL) |
|
if err != nil { |
|
log.Fatalln(errors.Wrap(err, "could not parse rootURL flag")) |
|
} |
|
} |
|
rl := rushlink{ |
|
db: db, |
|
fs: fs, |
|
rootURL: rootURL, |
|
} |
|
|
|
router := mux.NewRouter() |
|
router.Use(rl.metricsMiddleware) |
|
router.Use(rl.recoveryMiddleware) |
|
InitMainRouter(router, &rl) |
|
|
|
srv := &http.Server{ |
|
Handler: router, |
|
Addr: addr, |
|
WriteTimeout: 15 * time.Second, |
|
ReadTimeout: 15 * time.Second, |
|
} |
|
log.Fatal(srv.ListenAndServe()) |
|
}
|
|
|