diff --git a/package-lock.json b/package-lock.json index 7feab22..12ddbdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "bootstrap-icons": "^1.9.1", "bootstrap5-tags": "^1.6.1", "color-hash": "^2.0.2", + "modern-screenshot": "^4.4.30", "moment": "^2.29.4", "prismjs": "^1.29.0", "rapidoc": "^9.3.4", @@ -1717,6 +1718,11 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "optional": true }, + "node_modules/modern-screenshot": { + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/modern-screenshot/-/modern-screenshot-4.4.30.tgz", + "integrity": "sha512-rC6SC40NEP04qZqthKM+W3xttz3NuNOpyMFMZ+P//zoBxsSrQbrBGYL/Sp1h6U9TKmIzyj3vEymVwdcwl7EEiA==" + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", diff --git a/package.json b/package.json index c5c12e4..7ebd43b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "bootstrap-icons": "^1.9.1", "bootstrap5-tags": "^1.6.1", "color-hash": "^2.0.2", + "modern-screenshot": "^4.4.30", "moment": "^2.29.4", "prismjs": "^1.29.0", "rapidoc": "^9.3.4", diff --git a/server/handlers/proxy.go b/server/handlers/proxy.go new file mode 100644 index 0000000..292c9b6 --- /dev/null +++ b/server/handlers/proxy.go @@ -0,0 +1,147 @@ +// Package handlers contains a specific handlers +package handlers + +import ( + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + "time" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/utils/logger" +) + +var linkRe = regexp.MustCompile(`(?i)^https?:\/\/`) + +// ProxyHandler is used to proxy assets for printing +func ProxyHandler(w http.ResponseWriter, r *http.Request) { + uri := strings.TrimSpace(r.URL.Query().Get("url")) + if uri == "" { + logger.Log().Warn("[proxy] URL missing") + httpError(w, "Error: URL missing") + return + } + + if !linkRe.MatchString(uri) { + logger.Log().Warnf("[proxy] invalid URL %s", uri) + httpError(w, "Error: invalid URL") + return + } + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + logger.Log().Warnf("[proxy] %s", err.Error()) + httpError(w, err.Error()) + return + } + + // use requesting useragent + req.Header.Set("User-Agent", r.UserAgent()) + + resp, err := client.Do(req) + if err != nil { + logger.Log().Warnf("[proxy] %s", err.Error()) + httpError(w, err.Error()) + return + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + logger.Log().Warnf("[proxy] %s", err.Error()) + httpError(w, err.Error()) + return + } + + // relay common headers + if resp.Header.Get("content-type") != "" { + w.Header().Set("content-type", resp.Header.Get("content-type")) + } + if resp.Header.Get("last-modified") != "" { + w.Header().Set("last-modified", resp.Header.Get("last-modified")) + } + if resp.Header.Get("content-disposition") != "" { + w.Header().Set("content-disposition", resp.Header.Get("content-disposition")) + } + if resp.Header.Get("cache-control") != "" { + w.Header().Set("cache-control", resp.Header.Get("cache-control")) + } + + // replace url() values with proxy address, eg: fonts & images + if strings.HasPrefix(resp.Header.Get("content-type"), "text/css") { + var re = regexp.MustCompile(`(?mi)(url\((\'|\")?([^\)\'\"]+)(\'|\")?\))`) + body = re.ReplaceAllFunc(body, func(s []byte) []byte { + parts := re.FindStringSubmatch(string(s)) + + // don't resolve inline `data:..` + if strings.HasPrefix(parts[3], "data:") { + return []byte(parts[3]) + } + + address, err := absoluteURL(parts[3], uri) + if err != nil { + logger.Log().Error(err) + return []byte(parts[3]) + } + + return []byte("url(" + parts[2] + config.Webroot + "proxy?url=" + url.QueryEscape(address) + parts[4] + ")") + }) + } + + logger.Log().Debugf("[proxy] %s (%d)", uri, resp.StatusCode) + + // relay status code - WriteHeader must come after Header.Set() + w.WriteHeader(resp.StatusCode) + + w.Write(body) +} + +// AbsoluteURL will return a full URL regardless whether it is relative or absolute +func absoluteURL(link, baseURL string) (string, error) { + // scheme relative links, eg + +