wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-28 16:25:43 +04:00
parent 42c4e7bf84
commit 2776c41e0d
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
14 changed files with 430 additions and 13 deletions

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
@ -109,7 +110,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
OnlyOnce: true,
}
proxyHeadersListFlag = cli.StringFlag{
Name: "proxy-headers",
Name: "proxy-headers", // TODO: add support for the "*" wildcard
Usage: "listed here HTTP headers will be proxied from the original request to the error page response " +
"(comma-separated list)",
Value: strings.Join(cfg.ProxyHeaders, ","),
@ -235,6 +236,11 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
}
}
// check if there are any templates available to render error pages
if len(cfg.Templates.Names()) == 0 {
return errors.New("no templates available to render error pages")
}
// if the rotation mode is set to random-on-startup, pick a random template (ignore the user-provided
// template name)
if cfg.RotationMode == config.RotationModeRandomOnStartup {
@ -291,7 +297,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
}
// Run current command.
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error {
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error { //nolint:funlen
var srv = appHttp.NewServer(ctx, log)
if err := srv.Register(cfg); err != nil {
@ -301,6 +307,33 @@ func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Con
var startingErrCh = make(chan error, 1) // channel for server starting error
defer close(startingErrCh)
// to track the frequency of each template's use, we send a simple GET request to the GoatCounter API
// (https://goatcounter.com, https://github.com/arp242/goatcounter) to increment the counter. this service is
// free and does not require an API key. no private data is sent, as shown in the URL below. this is necessary
// to render a badge displaying the number of template usages on the error-pages repository README file :D
//
// badge code example:
// ![Used times](https://img.shields.io/badge/dynamic/json?
// url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Flost-in-space.json
// &query=%24.count&label=Used%20times)
go func() {
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(
"https://error-pages.goatcounter.com/count?event=true&p=/use-template/%s", url.QueryEscape(cfg.TemplateName),
), http.NoBody)
if reqErr != nil {
return
}
resp, respErr := (&http.Client{Timeout: 10 * time.Second}).Do(req) //nolint:mnd // don't care about the response
if respErr != nil {
log.Debug("Cannot send a request to increment the template usage counter", logger.Error(respErr))
return
} else if resp != nil {
_ = resp.Body.Close()
}
}()
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
var now = time.Now()

View File

@ -53,9 +53,12 @@ func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,
// disallow indexing of the error pages
w.Header().Set("X-Robots-Tag", "noindex")
if code >= 500 && code < 600 {
switch code {
case http.StatusRequestTimeout, http.StatusTooEarly, http.StatusTooManyRequests,
http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
// tell the client (search crawler) to retry the request after 120 seconds, it makes sense for the 5xx errors
// tell the client (search crawler) to retry the request after 120 seconds
w.Header().Set("Retry-After", "120")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,27 @@
package static
import (
_ "embed"
"net/http"
)
//go:embed favicon.ico
var Favicon []byte
// New creates a new handler that returns the provided content for GET and HEAD requests.
func New(content []byte) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", http.DetectContentType(content))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(content)
case http.MethodHead:
w.WriteHeader(http.StatusOK)
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
})
}

View File

@ -0,0 +1,80 @@
package static_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/http/handlers/static"
)
func TestServeHTTP(t *testing.T) {
t.Parallel()
var handler = static.New([]byte{1, 2, 3})
t.Run("get", func(t *testing.T) {
var (
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
rr = httptest.NewRecorder()
)
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Header().Get("Content-Type"), "application/octet-stream")
assert.Equal(t, rr.Code, http.StatusOK)
assert.Equal(t, rr.Body.Bytes(), []byte{1, 2, 3})
})
t.Run("head", func(t *testing.T) {
var (
req = httptest.NewRequest(http.MethodHead, "http://testing", http.NoBody)
rr = httptest.NewRecorder()
)
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Code, http.StatusOK)
assert.Empty(t, rr.Header().Get("Content-Type"))
assert.Empty(t, rr.Body.Bytes())
})
t.Run("method not allowed", func(t *testing.T) {
for _, method := range []string{
http.MethodDelete,
http.MethodPatch,
http.MethodPost,
http.MethodPut,
} {
var (
req = httptest.NewRequest(method, "http://testing", http.NoBody)
rr = httptest.NewRecorder()
)
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Header().Get("Content-Type"), "text/plain; charset=utf-8")
assert.Equal(t, rr.Code, http.StatusMethodNotAllowed)
assert.Equal(t, "Method Not Allowed\n", rr.Body.String())
}
})
}
func TestServeHTTP_Favicon(t *testing.T) {
t.Parallel()
var (
handler = static.New(static.Favicon)
req = httptest.NewRequest(http.MethodGet, "http://testing", http.NoBody)
rr = httptest.NewRecorder()
)
handler.ServeHTTP(rr, req)
assert.Equal(t, rr.Header().Get("Content-Type"), "image/x-icon")
assert.Equal(t, rr.Code, http.StatusOK)
assert.Equal(t, rr.Body.Bytes(), static.Favicon)
}

View File

@ -12,6 +12,7 @@ import (
"gh.tarampamp.am/error-pages/internal/config"
ep "gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
"gh.tarampamp.am/error-pages/internal/http/handlers/static"
"gh.tarampamp.am/error-pages/internal/http/handlers/version"
"gh.tarampamp.am/error-pages/internal/http/middleware/logreq"
"gh.tarampamp.am/error-pages/internal/logger"
@ -50,6 +51,7 @@ func (s *Server) Register(cfg *config.Config) error {
liveHandler = live.New()
versionHandler = version.New(appmeta.Version())
errorPagesHandler = ep.New(cfg, s.log)
faviconHandler = static.New(static.Favicon)
)
s.server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -64,6 +66,10 @@ func (s *Server) Register(cfg *config.Config) error {
case url == "/version":
versionHandler.ServeHTTP(w, r)
// favicon.ico endpoint
case url == "/favicon.ico":
faviconHandler.ServeHTTP(w, r)
// error pages endpoints:
// - /
// - /{code}.html
@ -87,8 +93,9 @@ func (s *Server) Register(cfg *config.Config) error {
// apply middleware
s.server.Handler = logreq.New(s.log, func(r *http.Request) bool {
// skip logging healthcheck requests
return strings.Contains(strings.ToLower(r.UserAgent()), "healthcheck")
// skip logging healthcheck and .ico (favicon) requests
return strings.Contains(strings.ToLower(r.UserAgent()), "healthcheck") ||
strings.HasSuffix(r.URL.Path, ".ico")
})(s.server.Handler)
return nil

View File

@ -6,7 +6,7 @@
<title>{{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30" />
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

View File

@ -6,7 +6,7 @@
<title>{{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30" />
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

View File

@ -6,7 +6,7 @@
<title>{{ code }} | {{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30" />
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

View File

@ -6,7 +6,7 @@
<title>{{ code }}: {{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30" />
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

View File

@ -6,7 +6,7 @@
<title>{{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30" />
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

View File

@ -6,7 +6,7 @@
<title>{{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30"/>
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

View File

@ -6,7 +6,7 @@
<title>{{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30"/>
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">

267
templates/shuffle.html Normal file
View File

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="nofollow,noarchive,noindex">
<title>{{ code }} - {{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {{ if or (eq code 408) (eq code 425) (eq code 429) (eq code 500) (eq code 502) (eq code 503) (eq code 504) }} -->
<meta http-equiv="refresh" content="30">
<!-- {{ end }} -->
<meta name="title" content="{{ code }}: {{ message | escape }}">
<meta name="description" content="{{ description | escape }}">
<meta property="og:title" content="{{ code }}: {{ message | escape }}">
<meta property="og:description" content="{{ description | escape }}">
<meta property="twitter:title" content="{{ code }}: {{ message | escape }}">
<meta property="twitter:description" content="{{ description | escape }}">
<style>
:root {
--color-primary: #eee;
--color-inverted: #222;
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #222;
--color-inverted: #aaa;
}
}
html, body {
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
width: 100%;
background-color: var(--color-primary);
color: var(--color-inverted);
font-family: monospace;
font-size: 16px;
}
@media screen and (min-width: 2000px) {
html, body {
font-size: 20px;
}
}
body {
display: flex;
justify-content: center;
align-items: center;
}
main {
display: flex;
}
article {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
article #error_text h1 {
font-size: 2em;
font-weight: normal;
padding: 0;
margin: 0;
}
/* {{ if show_details }} */
#details {
table-layout: fixed;
width: 100%;
opacity: .75;
margin-top: 1em;
}
#details td {
white-space: nowrap;
font-size: 0.7em;
transition: opacity 1.4s, font-size .3s;
}
#details.hidden td {
opacity: 0;
font-size: 0;
}
#details .name,
#details .value {
width: 50%;
}
#details .name::first-letter,
#details .value::first-letter {
font-weight: bold;
}
#details .name {
text-align: right;
padding-right: .2em;
width: 50%;
}
#details .value {
text-align: left;
padding-left: .4em;
font-family: monospace;
overflow: hidden;
text-overflow: ellipsis;
}
/* {{ end }} */
</style>
</head>
<body>
<main>
<article>
<div id="error_text">
<h1 class="source">{{ code }}: <span data-l10n>{{ message }}</span></h1>
<h1 class="target"></h1>
</div>
<!-- {{- if show_details -}} -->
<table id="details" class="hidden">
<!-- {{- if host -}} -->
<tr>
<td class="name"><span data-l10n>Host</span>:</td>
<td class="value">{{ host }}</td>
</tr>
<!-- {{- end }}{{ if original_uri -}} -->
<tr>
<td class="name"><span data-l10n>Original URI</span>:</td>
<td class="value">{{ original_uri }}</td>
</tr>
<!-- {{- end }}{{ if forwarded_for -}} -->
<tr>
<td class="name"><span data-l10n>Forwarded for</span>:</td>
<td class="value">{{ forwarded_for }}</td>
</tr>
<!-- {{- end }}{{ if namespace -}} -->
<tr>
<td class="name"><span data-l10n>Namespace</span>:</td>
<td class="value">{{ namespace }}</td>
</tr>
<!-- {{- end }}{{ if ingress_name -}} -->
<tr>
<td class="name"><span data-l10n>Ingress name</span>:</td>
<td class="value">{{ ingress_name }}</td>
</tr>
<!-- {{- end }}{{ if service_name -}} -->
<tr>
<td class="name"><span data-l10n>Service name</span>:</td>
<td class="value">{{ service_name }}</td>
</tr>
<!-- {{- end }}{{ if service_port -}} -->
<tr>
<td class="name"><span data-l10n>Service port</span>:</td>
<td class="value">{{ service_port }}</td>
</tr>
<!-- {{- end }}{{ if request_id -}} -->
<tr>
<td class="name"><span data-l10n>Request ID</span>:</td>
<td class="value">{{ request_id }}</td>
</tr>
<!-- {{- end -}} -->
<tr>
<td class="name"><span data-l10n>Timestamp</span>:</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
<!-- {{- end -}} -->
</article>
</main>
<script>
'use strict';
/**
* @param {HTMLElement} $el
*/
const Shuffle = function ($el) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=+<>,./?[{()}]!@#$%^&*~`\|'.split('');
const $source = $el.querySelector('.source');
const $target = $el.querySelector('.target');
let cursor = 0;
/** @type {Number|undefined} */
let scrambleInterval;
/** @type {Number|undefined} */
let cursorDelayInterval;
/** @type {Number|undefined} */
let cursorInterval;
/**
* @param {Number} len
* @return {string}
*/
const getRandomizedString = function (len) {
let s = '';
for (let i = 0; i < len; i++) {
s += chars[Math.floor(Math.random() * chars.length)];
}
return s;
};
this.start = function () {
$source.style.display = 'none';
$target.style.display = 'block';
scrambleInterval = window.setInterval(() => {
if (cursor <= $source.innerText.length) {
$target.innerText = $source.innerText.substring(0, cursor) + getRandomizedString($source.innerText.length - cursor);
}
}, 450 / 30);
cursorDelayInterval = window.setTimeout(() => {
cursorInterval = window.setInterval(() => {
if (cursor > $source.innerText.length - 1) {
this.stop();
}
cursor++;
}, 70);
}, 350);
};
this.stop = function () {
$source.style.display = 'block';
$target.style.display = 'none';
$target.innerText = '';
cursor = 0;
if (scrambleInterval !== undefined) {
window.clearInterval(scrambleInterval);
scrambleInterval = undefined;
}
if (cursorInterval !== undefined) {
window.clearInterval(cursorInterval);
cursorInterval = undefined;
}
if (cursorDelayInterval !== undefined) {
window.clearInterval(cursorDelayInterval);
cursorDelayInterval = undefined;
}
};
};
(new Shuffle(document.getElementById('error_text'))).start();
// {{ if show_details }}
window.setTimeout(function () {
document.getElementById('details').classList.remove('hidden');
}, 550);
// {{ end }}
</script>
<!-- {{- if l10n_enabled -}} -->
<script> // {{ l10nScript }}</script>
<!-- {{- end -}} -->
</body>
</html>