feat: add Go server implementation with HTTPS support and update README for usage instructions
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
A simple HTTP server for connectivity testing. It can be run from the command line or in a container.
|
||||
|
||||
The Python version relies only on Python's standard library, so it should work in any environment with Python installed. The Node.js version also relies only on the standard library, so it should work in any environment with Node.js installed.
|
||||
The Python version relies only on Python's standard library, so it should work in any environment with Python installed. The Node.js version also relies only on the standard library, so it should work in any environment with Node.js installed. The Go version also relies only on the standard library, so it should work in any environment with Go installed.
|
||||
|
||||
It displays a simple HTML or plain-text page with the client's IP address and any detected `X-*` headers.
|
||||
|
||||
@@ -20,7 +20,8 @@ To run the server from the command line:
|
||||
python3 ok-server.py
|
||||
python3 ok-server.py --look basic
|
||||
python3 ok-server.py --bind 127.0.0.1 --port 8080 --look tailwind
|
||||
python3 ok-server.py --pem /path/to/bundle.pem --port 8443
|
||||
python3 ok-server.py --pem /path/to/bundle.pem
|
||||
python3 ok-server.py --pem /path/to/bundle.pem --tls-port 9443
|
||||
```
|
||||
|
||||
`--look` accepts `basic`, `nice`, `bootstrap`, or `tailwind`.
|
||||
@@ -28,13 +29,22 @@ You can override the look per request with the `look` query parameter, for examp
|
||||
`http://localhost:8080/?look=tailwind`
|
||||
|
||||
`--pem` accepts a path to a PEM file containing the certificate chain and private key.
|
||||
When provided, the server listens on HTTPS instead of plain HTTP.
|
||||
When provided, the server listens on both HTTP (`--port`, default 8080) and HTTPS (`--tls-port`, default 8443).
|
||||
|
||||
```bash
|
||||
node ok-server.mjs
|
||||
node ok-server.mjs --look basic
|
||||
node ok-server.mjs --bind 127.0.0.1 --port 8080 --look tailwind
|
||||
node ok-server.mjs --pem /path/to/bundle.pem --port 8443
|
||||
node ok-server.mjs --pem /path/to/bundle.pem
|
||||
node ok-server.mjs --pem /path/to/bundle.pem --tls-port 9443
|
||||
```
|
||||
|
||||
```bash
|
||||
go run ok-server.go
|
||||
go run ok-server.go --look basic
|
||||
go run ok-server.go --bind 127.0.0.1 --port 8080 --look tailwind
|
||||
go run ok-server.go --pem /path/to/bundle.pem
|
||||
go run ok-server.go --pem /path/to/bundle.pem --tls-port 9443
|
||||
```
|
||||
|
||||
Connect to the server using a web browser or a tool like `curl`:
|
||||
@@ -43,6 +53,27 @@ Connect to the server using a web browser or a tool like `curl`:
|
||||
curl -si "http://localhost:8080"
|
||||
```
|
||||
|
||||
### Building a Go binary
|
||||
|
||||
Cross-compile for any platform from any machine:
|
||||
|
||||
```bash
|
||||
# Linux amd64
|
||||
GOOS=linux GOARCH=amd64 go build -o ok-server-linux-amd64 ok-server.go
|
||||
|
||||
# Linux arm64
|
||||
GOOS=linux GOARCH=arm64 go build -o ok-server-linux-arm64 ok-server.go
|
||||
|
||||
# macOS arm64 (Apple Silicon)
|
||||
GOOS=darwin GOARCH=arm64 go build -o ok-server-darwin-arm64 ok-server.go
|
||||
|
||||
# macOS amd64 (Intel)
|
||||
GOOS=darwin GOARCH=amd64 go build -o ok-server-darwin-amd64 ok-server.go
|
||||
|
||||
# Windows amd64
|
||||
GOOS=windows GOARCH=amd64 go build -o ok-server-windows-amd64.exe ok-server.go
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Use the included `Dockerfile` to build and run the server in a Docker container:
|
||||
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
// Simple HTTP server that responds 200 with a test message including client IP.
|
||||
//
|
||||
// Behavior:
|
||||
// - GET returns HTTP/1.1 200 with a message including the requester IP
|
||||
// - HEAD returns the same headers as GET but no body
|
||||
// - Displays incoming X-* headers when present
|
||||
// - If User-Agent contains "curl" or "wget" (case-insensitive), the server
|
||||
// responds with Content-Type: text/plain; otherwise Content-Type: text/html.
|
||||
// - --pem specifies a PEM bundle file (cert chain + private key). When provided,
|
||||
// the server listens on both HTTP (--port) and HTTPS (--tls-port).
|
||||
// Without --pem the server listens on plain HTTP only.
|
||||
// - --look controls which HTML variant is returned for non-CLI agents:
|
||||
// - basic - plain HTML with no external references
|
||||
// - nice - includes Google Font "Noto Sans" (default)
|
||||
// - bootstrap - Bootstrap 5 layout
|
||||
// - tailwind - Tailwind CSS via @tailwindcss/browser@latest
|
||||
// - The URL query parameter "look" (e.g. /?look=nice) overrides --look for
|
||||
// that request only. Values are case-insensitive and must be basic,nice,bootstrap,tailwind.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// go run ok-server.go # binds 0.0.0.0:8080, nice look
|
||||
// go run ok-server.go --look basic
|
||||
// go run ok-server.go -b 127.0.0.1 -p 8080 --look tailwind
|
||||
// go run ok-server.go --pem /path/to/bundle.pem # HTTP :8080 + HTTPS :8443
|
||||
// go run ok-server.go --pem /path/to/bundle.pem --tls-port 9443
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const plainTemplate = "Hello, This is a test HTTP server.\n\nYour request came from {ip}.\n\n{proxy_headers_block}Have a nice day!\n"
|
||||
|
||||
const htmlBasic = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test HTTP server</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello,</h1>
|
||||
<p>This is a test HTTP server.</p>
|
||||
<p>Your request came from {ip_html}.</p>
|
||||
{proxy_headers_html}
|
||||
<p>Have a nice day!</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const htmlNice = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Test HTTP server</title>
|
||||
<!-- Google Font: Noto Sans -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body { font-family: "Noto Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; padding: 2rem; background: #f6f7fb; }
|
||||
.card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); max-width: 800px; margin: 2rem auto; }
|
||||
h1 { margin-top: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Hello,</h1>
|
||||
<p>This is a test HTTP server.</p>
|
||||
<p>Your request came from {ip_html}.</p>
|
||||
{proxy_headers_html}
|
||||
<p>Have a nice day!</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const htmlBootstrap = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Test HTTP server</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">Test HTTP server</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Hello,</h1>
|
||||
<p class="card-text">This is a test HTTP server.</p>
|
||||
<p class="card-text">Your request came from {ip_html}.</p>
|
||||
{proxy_headers_html}
|
||||
<hr>
|
||||
<p class="mb-0">Have a nice day!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="text-center mt-4 mb-4">
|
||||
<small class="text-muted">Simple status page for firewall/testing</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const htmlTailwind = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Test HTTP server</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@latest"></script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-100 text-slate-900">
|
||||
<div class="mx-auto max-w-3xl px-4 py-10 sm:py-16">
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-lg">
|
||||
<div class="bg-slate-900 px-6 py-4 text-slate-100">
|
||||
<h1 class="text-xl font-semibold">Test HTTP server</h1>
|
||||
<p class="mt-1 text-sm text-slate-300">Simple status page for firewall/testing</p>
|
||||
</div>
|
||||
<main class="space-y-4 px-6 py-6">
|
||||
<p class="text-2xl font-bold">Hello,</p>
|
||||
<p>This is a test HTTP server.</p>
|
||||
<p>Your request came from {ip_html}.</p>
|
||||
{proxy_headers_html}
|
||||
<p class="pt-2">Have a nice day!</p>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
var lookTemplates = map[string]string{
|
||||
"basic": htmlBasic,
|
||||
"nice": htmlNice,
|
||||
"bootstrap": htmlBootstrap,
|
||||
"tailwind": htmlTailwind,
|
||||
}
|
||||
|
||||
func getClientIP(r *http.Request) string {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func isCliAgent(ua string) bool {
|
||||
ua = strings.ToLower(ua)
|
||||
return strings.Contains(ua, "curl") || strings.Contains(ua, "wget")
|
||||
}
|
||||
|
||||
func getRequestLook(r *http.Request) string {
|
||||
look := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("look")))
|
||||
if _, ok := lookTemplates[look]; ok {
|
||||
return look
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func collectXHeaders(r *http.Request) [][2]string {
|
||||
var result [][2]string
|
||||
for name, values := range r.Header {
|
||||
if strings.HasPrefix(strings.ToLower(name), "x-") {
|
||||
result = append(result, [2]string{name, strings.Join(values, ", ")})
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return strings.ToLower(result[i][0]) < strings.ToLower(result[j][0])
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func proxyMarkup(xHeaders [][2]string) (string, string) {
|
||||
if len(xHeaders) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
lines := make([]string, len(xHeaders))
|
||||
for i, h := range xHeaders {
|
||||
lines[i] = fmt.Sprintf(" - %s: %s", h[0], h[1])
|
||||
}
|
||||
block := "Reverse proxy condition: detected via X-* headers.\nX-* headers:\n" +
|
||||
strings.Join(lines, "\n") + "\n\n"
|
||||
|
||||
htmlItems := make([]string, len(xHeaders))
|
||||
for i, h := range xHeaders {
|
||||
htmlItems[i] = fmt.Sprintf("<li><code>%s</code>: %s</li>",
|
||||
html.EscapeString(h[0]), html.EscapeString(h[1]))
|
||||
}
|
||||
htmlStr := "<p>Reverse proxy condition: <strong>detected via X-* headers</strong>.</p>" +
|
||||
"<p>X-* headers:</p><ul>" + strings.Join(htmlItems, "") + "</ul>"
|
||||
|
||||
return block, htmlStr
|
||||
}
|
||||
|
||||
func makeBodyAndType(r *http.Request, clientIP, defaultLook string) ([]byte, string) {
|
||||
ua := r.Header.Get("User-Agent")
|
||||
xHeaders := collectXHeaders(r)
|
||||
proxyHeadersBlock, proxyHeadersHTML := proxyMarkup(xHeaders)
|
||||
|
||||
var ipText, ipHTML string
|
||||
xff := strings.TrimSpace(strings.SplitN(r.Header.Get("X-Forwarded-For"), ",", 2)[0])
|
||||
if xff != "" {
|
||||
realIP := xff
|
||||
if strings.HasPrefix(realIP, "[") {
|
||||
if idx := strings.Index(realIP, "]"); idx != -1 {
|
||||
realIP = realIP[1:idx]
|
||||
} else {
|
||||
realIP = realIP[1:]
|
||||
}
|
||||
} else if strings.Count(realIP, ":") == 1 {
|
||||
realIP = strings.SplitN(realIP, ":", 2)[0]
|
||||
}
|
||||
ipText = fmt.Sprintf("%s (forwarded by proxy %s)", realIP, clientIP)
|
||||
ipHTML = fmt.Sprintf("<strong>%s</strong> (forwarded by proxy <strong>%s</strong>)",
|
||||
html.EscapeString(realIP), html.EscapeString(clientIP))
|
||||
} else {
|
||||
ipText = clientIP
|
||||
ipHTML = fmt.Sprintf("<strong>%s</strong>", html.EscapeString(clientIP))
|
||||
}
|
||||
|
||||
if isCliAgent(ua) {
|
||||
body := strings.NewReplacer(
|
||||
"{ip}", ipText,
|
||||
"{proxy_headers_block}", proxyHeadersBlock,
|
||||
).Replace(plainTemplate)
|
||||
return []byte(body), "text/plain; charset=utf-8"
|
||||
}
|
||||
|
||||
look := getRequestLook(r)
|
||||
if look == "" {
|
||||
look = defaultLook
|
||||
}
|
||||
body := strings.NewReplacer(
|
||||
"{ip_html}", ipHTML,
|
||||
"{proxy_headers_html}", proxyHeadersHTML,
|
||||
).Replace(lookTemplates[look])
|
||||
return []byte(body), "text/html; charset=utf-8"
|
||||
}
|
||||
|
||||
func makeHandler(defaultLook string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP := getClientIP(r)
|
||||
now := time.Now().UTC().Format("02/Jan/2006:15:04:05 +0000")
|
||||
fmt.Fprintf(os.Stderr, "%s - - [%s] \"%s %s\"\n", clientIP, now, r.Method, r.URL.RequestURI())
|
||||
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
body, contentType := makeBodyAndType(r, clientIP, defaultLook)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method != http.MethodHead {
|
||||
w.Write(body) //nolint:errcheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
fs := flag.NewFlagSet("ok-server", flag.ContinueOnError)
|
||||
fs.Usage = func() {
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" go run ok-server.go [-b|--bind ADDRESS] [-p|--port PORT] [--tls-port PORT] [--pem PEMFILE] [--look basic|nice|bootstrap|tailwind]")
|
||||
fmt.Println()
|
||||
fmt.Println("Defaults:")
|
||||
fmt.Println(" --bind 0.0.0.0")
|
||||
fmt.Println(" --port 8080")
|
||||
fmt.Println(" --tls-port 8443")
|
||||
fmt.Println(" --pem (none, plain HTTP only)")
|
||||
fmt.Println(" --look nice")
|
||||
}
|
||||
|
||||
var bind, look, pemFile string
|
||||
var port, tlsPort int
|
||||
fs.StringVar(&bind, "b", "0.0.0.0", "address to bind to")
|
||||
fs.StringVar(&bind, "bind", "0.0.0.0", "address to bind to")
|
||||
fs.IntVar(&port, "p", 8080, "HTTP port")
|
||||
fs.IntVar(&port, "port", 8080, "HTTP port")
|
||||
fs.IntVar(&tlsPort, "tls-port", 8443, "HTTPS port (when --pem is provided)")
|
||||
fs.StringVar(&pemFile, "pem", "", "PEM bundle (cert+key); enables HTTPS alongside HTTP")
|
||||
fs.StringVar(&look, "look", "nice", "default HTML look: basic|nice|bootstrap|tailwind")
|
||||
|
||||
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||
if err == flag.ErrHelp {
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if _, ok := lookTemplates[look]; !ok {
|
||||
fmt.Fprintf(os.Stderr, "Invalid look: %q (expected one of: basic, nice, bootstrap, tailwind)\n", look)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
handler := makeHandler(look)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", bind, port),
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
var httpsSrv *http.Server
|
||||
if pemFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(pemFile, pemFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Cannot load PEM file %q: %v\n", pemFile, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
httpsSrv = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", bind, tlsPort),
|
||||
Handler: handler,
|
||||
TLSConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
fmt.Printf("Serving on http://%s:%d (default look=%s)\n", bind, port, look)
|
||||
|
||||
if httpsSrv != nil {
|
||||
go func() {
|
||||
if err := httpsSrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "HTTPS server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
fmt.Printf("Serving on https://%s:%d (default look=%s)\n", bind, tlsPort, look)
|
||||
}
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
fmt.Println("\nShutting down server")
|
||||
ctx := context.Background()
|
||||
httpSrv.Shutdown(ctx)
|
||||
if httpsSrv != nil {
|
||||
httpsSrv.Shutdown(ctx)
|
||||
}
|
||||
}
|
||||
+62
-31
@@ -8,8 +8,9 @@
|
||||
* - Displays incoming X-* headers when present
|
||||
* - If User-Agent contains "curl" or "wget" (case-insensitive), the server
|
||||
* responds with Content-Type: text/plain; otherwise Content-Type: text/html.
|
||||
* - --pem specifies a PEM bundle file (cert chain + private key) to enable HTTPS.
|
||||
* Without --pem the server listens on plain HTTP.
|
||||
* - --pem specifies a PEM bundle file (cert chain + private key). When provided,
|
||||
* the server listens on both HTTP (--port) and HTTPS (--tls-port).
|
||||
* Without --pem the server listens on plain HTTP only.
|
||||
* - --look controls which HTML variant is returned for non-CLI agents:
|
||||
* * basic - plain HTML with no external references
|
||||
* * nice - includes Google Font "Noto Sans" (default)
|
||||
@@ -261,6 +262,7 @@ function getClientIp(req) {
|
||||
function parseArgs(argv) {
|
||||
let bind = "0.0.0.0";
|
||||
let port = 8080;
|
||||
let tlsPort = 8443;
|
||||
let look = "nice";
|
||||
let pem = null;
|
||||
|
||||
@@ -298,6 +300,19 @@ function parseArgs(argv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--tls-port") {
|
||||
i += 1;
|
||||
if (i >= argv.length) {
|
||||
throw new Error(`${arg} requires a value`);
|
||||
}
|
||||
const parsedTlsPort = Number.parseInt(argv[i], 10);
|
||||
if (!Number.isInteger(parsedTlsPort) || parsedTlsPort < 1 || parsedTlsPort > 65535) {
|
||||
throw new Error(`Invalid tls-port: ${argv[i]}`);
|
||||
}
|
||||
tlsPort = parsedTlsPort;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--look") {
|
||||
i += 1;
|
||||
if (i >= argv.length) {
|
||||
@@ -314,12 +329,13 @@ function parseArgs(argv) {
|
||||
if (arg === "-h" || arg === "--help") {
|
||||
const help = [
|
||||
"Usage:",
|
||||
" node ok-server.mjs [-b|--bind ADDRESS] [-p|--port PORT] [--pem PEMFILE] [--look basic|nice|bootstrap|tailwind]",
|
||||
" node ok-server.mjs [-b|--bind ADDRESS] [-p|--port PORT] [--tls-port PORT] [--pem PEMFILE] [--look basic|nice|bootstrap|tailwind]",
|
||||
"",
|
||||
"Defaults:",
|
||||
" --bind 0.0.0.0",
|
||||
" --port 8080",
|
||||
" --pem (none, plain HTTP)",
|
||||
" --tls-port 8443",
|
||||
" --pem (none, plain HTTP only)",
|
||||
" --look nice",
|
||||
].join("\n");
|
||||
console.log(help);
|
||||
@@ -329,11 +345,12 @@ function parseArgs(argv) {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
return { bind, port, look, pem };
|
||||
return { bind, port, tlsPort, look, pem };
|
||||
}
|
||||
|
||||
function run(bind, port, look, pem) {
|
||||
function run(bind, port, tlsPort, look, pem) {
|
||||
let isShuttingDown = false;
|
||||
const servers = [];
|
||||
|
||||
function createRequestHandler(req, res) {
|
||||
const method = req.method || "GET";
|
||||
@@ -360,7 +377,32 @@ function run(bind, port, look, pem) {
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
let server;
|
||||
function setupServer(server, scheme, listenPort) {
|
||||
server.on("request", (req) => {
|
||||
const clientIp = getClientIp(req);
|
||||
const now = new Date().toUTCString();
|
||||
const method = req.method || "GET";
|
||||
const path = req.url || "/";
|
||||
process.stderr.write(`${clientIp} - - [${now}] "${method} ${path}"\n`);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
process.stderr.write(`Server error (${scheme}): ${err.message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
server.listen(listenPort, bind, () => {
|
||||
console.log(`Serving on ${scheme}://${bind}:${listenPort} (default look=${look})`);
|
||||
});
|
||||
|
||||
servers.push(server);
|
||||
}
|
||||
|
||||
// Create HTTP server
|
||||
const httpServer = http.createServer(createRequestHandler);
|
||||
setupServer(httpServer, "http", port);
|
||||
|
||||
// Create HTTPS server if PEM is provided
|
||||
if (pem) {
|
||||
let pemContent;
|
||||
try {
|
||||
@@ -368,36 +410,25 @@ function run(bind, port, look, pem) {
|
||||
} catch (err) {
|
||||
throw new Error(`Cannot read PEM file '${pem}': ${err.message}`);
|
||||
}
|
||||
server = https.createServer({ cert: pemContent, key: pemContent }, createRequestHandler);
|
||||
} else {
|
||||
server = http.createServer(createRequestHandler);
|
||||
const httpsServer = https.createServer({ cert: pemContent, key: pemContent }, createRequestHandler);
|
||||
setupServer(httpsServer, "https", tlsPort);
|
||||
}
|
||||
|
||||
server.on("request", (req) => {
|
||||
const clientIp = getClientIp(req);
|
||||
const now = new Date().toUTCString();
|
||||
const method = req.method || "GET";
|
||||
const path = req.url || "/";
|
||||
process.stderr.write(`${clientIp} - - [${now}] "${method} ${path}"\n`);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
process.stderr.write(`Server error: ${err.message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
server.listen(port, bind, () => {
|
||||
const scheme = pem ? "https" : "http";
|
||||
console.log(`Serving on ${scheme}://${bind}:${port} (default look=${look}) (Ctrl-C to stop)`);
|
||||
});
|
||||
|
||||
function handleShutdownSignal() {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
isShuttingDown = true;
|
||||
console.log("\nShutting down server");
|
||||
server.close(() => process.exit(0));
|
||||
let closed = 0;
|
||||
servers.forEach((server) => {
|
||||
server.close(() => {
|
||||
closed += 1;
|
||||
if (closed === servers.length) {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
process.on("SIGINT", handleShutdownSignal);
|
||||
@@ -405,8 +436,8 @@ function run(bind, port, look, pem) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { bind, port, look, pem } = parseArgs(process.argv.slice(2));
|
||||
run(bind, port, look, pem);
|
||||
const { bind, port, tlsPort, look, pem } = parseArgs(process.argv.slice(2));
|
||||
run(bind, port, tlsPort, look, pem);
|
||||
} catch (err) {
|
||||
process.stderr.write(`${err.message}\n`);
|
||||
process.exit(2);
|
||||
|
||||
+49
-28
@@ -7,8 +7,9 @@ Behavior:
|
||||
- Displays whether any incoming X-* headers were detected and lists them
|
||||
- If User-Agent contains "curl" or "wget" (case-insensitive) the server
|
||||
responds with Content-Type: text/plain; otherwise Content-Type: text/html.
|
||||
- --pem path to a PEM bundle (cert chain + private key) to enable HTTPS;
|
||||
without --pem the server uses plain HTTP.
|
||||
- --pem path to a PEM bundle (cert chain + private key). When provided,
|
||||
the server listens on both HTTP (--port) and HTTPS (--tls-port).
|
||||
Without --pem the server listens on plain HTTP only.
|
||||
- --look controls which HTML variant is returned for non-CLI agents:
|
||||
* basic - plain HTML with no external references
|
||||
* nice - includes Google Font "Noto Sans" (default)
|
||||
@@ -18,9 +19,11 @@ Behavior:
|
||||
for that request only. Values are case-insensitive and must be one of basic,nice,bootstrap,tailwind.
|
||||
|
||||
Usage:
|
||||
python3 ok_server.py # binds 0.0.0.0:8080, nice look
|
||||
python3 ok_server.py # binds 0.0.0.0:8080, nice look
|
||||
python3 ok_server.py --look basic
|
||||
python3 ok_server.py -b 127.0.0.1 -p 8080 --look tailwind
|
||||
python3 ok_server.py --pem /path/to/bundle.pem # HTTP :8080 + HTTPS :8443
|
||||
python3 ok_server.py --pem /path/to/bundle.pem --tls-port 9443
|
||||
|
||||
Test:
|
||||
curl -i http://localhost:8080/ # text/plain for curl
|
||||
@@ -34,6 +37,7 @@ import argparse
|
||||
import signal
|
||||
import ssl
|
||||
import sys
|
||||
import threading
|
||||
from html import escape
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Tuple
|
||||
@@ -306,35 +310,50 @@ class OkHandler(BaseHTTPRequestHandler):
|
||||
(self.client_address[0], self.log_date_time_string(), format % args))
|
||||
|
||||
|
||||
def run(bind: str, port: int, look: str, pem: str | None = None):
|
||||
addr = (bind, port)
|
||||
def run(bind: str, port: int, tls_port: int, look: str, pem: str | None = None):
|
||||
ssl_ctx = None
|
||||
if pem:
|
||||
try:
|
||||
ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_ctx.load_cert_chain(pem)
|
||||
except (ssl.SSLError, OSError) as e:
|
||||
print(f"Cannot load PEM file '{pem}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
http_server = ThreadingHTTPServer((bind, port), OkHandler)
|
||||
http_server.look = look
|
||||
https_server = None
|
||||
|
||||
if ssl_ctx:
|
||||
https_server = ThreadingHTTPServer((bind, tls_port), OkHandler)
|
||||
https_server.look = look
|
||||
https_server.socket = ssl_ctx.wrap_socket(https_server.socket, server_side=True)
|
||||
|
||||
def _handle_termination(signum, _frame):
|
||||
_ = signum
|
||||
if https_server:
|
||||
https_server.shutdown()
|
||||
raise KeyboardInterrupt
|
||||
|
||||
signal.signal(signal.SIGINT, _handle_termination)
|
||||
signal.signal(signal.SIGTERM, _handle_termination)
|
||||
|
||||
try:
|
||||
with ThreadingHTTPServer(addr, OkHandler) as httpd:
|
||||
def _handle_termination(signum, _frame):
|
||||
_ = signum
|
||||
raise KeyboardInterrupt
|
||||
|
||||
signal.signal(signal.SIGINT, _handle_termination)
|
||||
signal.signal(signal.SIGTERM, _handle_termination)
|
||||
|
||||
# attach chosen look to server so handlers can use it as default
|
||||
httpd.look = look
|
||||
if pem:
|
||||
try:
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.load_cert_chain(pem)
|
||||
except (ssl.SSLError, OSError) as e:
|
||||
print(f"Cannot load PEM file '{pem}': {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
||||
scheme = "https" if pem else "http"
|
||||
print(f"Serving on {scheme}://{bind}:{port} (default look={look}) (Ctrl-C to stop)")
|
||||
httpd.serve_forever()
|
||||
print(f"Serving on http://{bind}:{port} (default look={look})")
|
||||
if https_server:
|
||||
print(f"Serving on https://{bind}:{tls_port} (default look={look})")
|
||||
t = threading.Thread(target=https_server.serve_forever, daemon=True)
|
||||
t.start()
|
||||
http_server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down server")
|
||||
except Exception as e:
|
||||
print("Server error:", e, file=sys.stderr)
|
||||
raise
|
||||
finally:
|
||||
http_server.server_close()
|
||||
if https_server:
|
||||
https_server.server_close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -345,7 +364,9 @@ if __name__ == "__main__":
|
||||
help="Port to listen on (default: 8080)")
|
||||
parser.add_argument("--look", choices=VALID_LOOKS, default=VALID_LOOKS[1],
|
||||
help="Default HTML look for non-cli agents (basic, nice, bootstrap, tailwind). Default: nice")
|
||||
parser.add_argument("--tls-port", type=int, default=8443, dest="tls_port",
|
||||
help="TLS port to listen on when --pem is provided (default: 8443)")
|
||||
parser.add_argument("--pem", default=None, metavar="PEMFILE",
|
||||
help="PEM bundle file (cert chain + private key) to enable HTTPS; plain HTTP if omitted")
|
||||
help="PEM bundle file (cert chain + private key); enables HTTPS on --tls-port alongside HTTP on --port")
|
||||
args = parser.parse_args()
|
||||
run(args.bind, args.port, args.look, args.pem)
|
||||
run(args.bind, args.port, args.tls_port, args.look, args.pem)
|
||||
|
||||
Reference in New Issue
Block a user