Initial Commit

This commit is contained in:
2025-09-28 22:45:26 +02:00
commit ceef69ca09
13 changed files with 456 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Build output
main.exe

10
Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM golang:1.25.0-alpine
RUN apk add --no-cache typst
WORKDIR /app
COPY . .
RUN go build -o main ./cmd/main.go
EXPOSE 8080
CMD ["./main", "/var/www/typst"]

134
cmd/main.go Normal file
View File

@@ -0,0 +1,134 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func jailPath(wwwdata string, path string) (string, error) {
base, err := filepath.Abs(wwwdata)
if err != nil {
return "", err
}
// Join and clean the path
joined := filepath.Join(base, path)
clean := filepath.Clean(joined)
// Ensure it's inside base, otherwise clamp to base
if !strings.HasPrefix(clean+string(os.PathSeparator), base+string(os.PathSeparator)) {
return base, nil
}
return clean, nil
}
func main() {
wwwdata, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting current working directory: %v\n", err)
os.Exit(1)
}
if len(os.Args) > 1 {
wwwdata = os.Args[1]
}
fmt.Fprintf(os.Stderr, "Serving files from: %s\n", wwwdata)
// Wildcard route to handle all GET requests
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Log time, method, path, ip, user-agent to stderr
fmt.Fprintf(os.Stderr, "%s - %s %s - %s - %s\n", time.Now().Format(time.RFC3339), r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
if path[len(path)-1] == '/' {
path += "index.typ"
}
// Convert the path to absolute path
filePath, err := jailPath(wwwdata, path)
if err != nil {
http.Error(w, "Internal Server Error: Failed to jail request", http.StatusInternalServerError)
return
}
// Check if the file exists and is not a directory
info, err := os.Stat(filePath)
if os.IsNotExist(err) || info.IsDir() {
// Check if a 404.typ file exists in the root directory
path404 := filepath.Join(wwwdata, "404.typ")
if _, err := os.Stat(path404); err == nil {
filePath = path404
w.WriteHeader(http.StatusNotFound)
} else {
http.NotFound(w, r)
return
}
}
headers := make(map[string]string)
for k, v := range r.Header {
headers[strings.ToLower(k)] = strings.Join(v[:], ",")
}
query := make(map[string]string)
for k, v := range r.URL.Query() {
query[k] = strings.Join(v[:], ",")
}
// Format request data as json for typst input
data := struct {
Method string `json:"method"`
Path string `json:"path"`
Headers map[string]string `json:"headers"`
Query map[string]string `json:"query"`
Timestamp string `json:"timestamp"`
Ip string `json:"ip,omitempty"`
Host string `json:"host,omitempty"`
}{
Method: r.Method,
Path: r.URL.Path,
Headers: headers,
Query: query,
Timestamp: time.Now().Format(time.RFC3339),
Ip: r.RemoteAddr,
Host: r.Host,
}
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, "Internal Server Error: Failed to parse request data", http.StatusInternalServerError)
return
}
jsonString := string(jsonData)
fontsdir := filepath.Join(wwwdata, "fonts")
// Compile the response
cmd := exec.Command("typst", "compile", filePath, "--font-path", fontsdir, "--input", "request="+jsonString, "-")
// Read stdout and send it to the response
cmd.Stdout = w
cmd.Stderr = os.Stderr
w.Header().Set("Content-Type", "application/pdf")
if err := cmd.Run(); err != nil {
http.Error(w, "Internal Server Error: Failed to compile document", http.StatusInternalServerError)
return
}
})
http.ListenAndServe(":8080", nil)
}

View File

@@ -0,0 +1,9 @@
services:
typstframework:
build: .
ports:
- "5082:8080"
volumes:
- ./example:/var/www/typst

58
example/404.typ Normal file
View File

@@ -0,0 +1,58 @@
#import "lib.typ"
#show: lib.ieee.with(
title: [Error 404: Not found],
abstract: [
Sorry this page could not be found!
],
authors: (
(
name: "Michael",
department: [Inventor of this Bullshit],
organization: [mikee.dev],
email: "typst@mikee.dev"
),
),
index-terms: ("Not found", "Sorry"),
)
#show: lib.styling
#image("assets/404.jpg")
= Introduction
... Well, this is awkward.
You've ventured into uncharted territory. The link you clicked, the URL you typed, or the button you mashed has led you to the digital equivalent of a blank spot on an ancient map, a place where pixels fear to tread.
Our servers, diligent and tireless digital librarians that they are, have scoured every nook and cranny of this domain. They've checked under dusty virtual carpets, peered behind forgotten firewalls, and even sent a search party into the deepest recesses of the sitemap. They returned with nothing but digital dust bunnies and a solemn shake of their virtual heads.
The page you were looking for is simply not here.
What could have happened?
= There are several cosmic possibilities for this predicament:
A Typographical Anomaly: Perhaps a single character in the URL was mistyped. In the vastness of the internet, even the smallest error can lead one light-years off course. Please check the address bar for any rogue letters or numbers.
An Interdimensional Shift: The page may have been moved or renamed during a recent restructuring of our corner of the web. Content is like a restless spirit; it sometimes migrates to a new home without leaving a forwarding address.
A Deliberate Decommissioning: The page you seek may have fulfilled its purpose and has now been retired, sent to a quiet digital farm upstate where it can run free with old GeoCities pages and Flash animations. It has ceased to be. It is an ex-page.
A Glitch in the Matrix: For a fleeting moment, reality flickered, and the universe decided this particular URL should lead to a void. We recommend looking over your shoulder to ensure you're not currently being pursued by agents in sunglasses.
= What should you do now?
Don't despair! Your journey doesn't have to end here. Here are a few paths back to civilization:
Retrace Your Steps: Use the back button in your browser to return to the last known point of safety.
Start Anew: Journey to our homepage and begin your quest again. It's a warm, well-lit place with clearly marked signs.
Consult the Oracle (Our Search Bar): Use the search function on our site. It possesses great wisdom and can often find what has been lost.
Contact Us: If you are certain this page should exist, please let us know. We'll dispatch our team of digital archeologists to investigate immediately.
We sincerely apologize for this detour into the digital void. We hope you find what you're looking for.

BIN
example/assets/404.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
example/assets/nyan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

4
example/debug.typ Normal file
View File

@@ -0,0 +1,4 @@
#import "lib.typ"
#lib.request

0
example/fonts/.gitkeep Normal file
View File

131
example/index.typ Normal file
View File

@@ -0,0 +1,131 @@
#import "@preview/cades:0.3.0": qr-code
#import "@preview/digestify:0.1.0": *
#import "@preview/suiji:0.4.0": *
#import "@preview/cetz:0.3.4"
#import "lib.typ"
#show: lib.ieee.with(
title: [Typst Framework: A web server running typst],
abstract: [
Typst framework is a go web server that uses typst as its scripting language. It's like PHP but for the modern web!
],
authors: (
(
name: "Michael",
department: [Inventor of this Bullshit],
organization: [mikee.dev],
email: "typst@mikee.dev"
),
),
index-terms: ("Web-Technology", "Shitpost"),
figure-supplement: [Fig.],
)
#show: lib.styling
= Introduction
Hey there! What a nice #datetime.today().display("[weekday]")! It's currently #lib.currentDateTime.display("[hour]:[minute]") and I'm feeling great! #if lib.currentDateTime.hour() <= 6 {
"(I should probably still be sleeping, but I'm feeling great regardless!)"
} else if lib.currentDateTime.hour() <= 8 {
"I just got up, not sure what to eat for breakfeast yet..."
} else if lib.currentDateTime.hour() <= 11 {
"I'm just getting started with my day, what about you?"
} else if lib.currentDateTime.hour() <= 12 {
"Just had lunch, yum."
} else if lib.currentDateTime.hour() <= 16 {
"I'm kind of in the middle of something here."
} else if lib.currentDateTime.hour() <= 20 {
"Watching YouTube rn, wbu?"
} else {
"But tired, I'm going to sleep soon."
} Anyways, allow me to introduce myself: My name is Typst and I am v#sys.version old.
= Introduction
Please allow me to intruduce *yourself* now!
Your user agent is
#box(
raw(
lib.ua,
block: true
),
fill: rgb("#ddd"),
inset: 1em
)
Hm... that must mean you are using *#lib.browser* on *#lib.platform*! Wow, how exciting, I love #lib.browser!!
If you were to look at your URL bar, it probably says something boring like "#lib.request.path", you could change that by #link("./page2.typ")[clicking here], you know...
= Fun games
Here is a QR code with your IP address!
#align(center)[
#qr-code(lib.request.ip, width: 3cm)
]
#if lib.request.headers.at("cf-connecting-ip", default: "").len() > 0 {
text("But this website is behind two HTTP proxies, so here is a SHA256 hash of your real IP: ")
bytes-to-hex(sha256(bytes(lib.request.headers.at("cf-connecting-ip", default: false))))
}
Here are some cute drawings I made!
#let seed1 = int(lib.currentDateTime.display().replace(" ", "").replace("-", "").replace(":", ""))
#let seed2 = array(sha256(bytes(lib.ua + lib.request.headers.at("accept-language", default: "")))).fold(0, (acc, b) => acc * 2 + b)
This one changes every second:
#align(center)[
#cetz.canvas(length: 12pt, {
import cetz.draw: *
let n = 20
let (x, y) = (0, 0)
let (x-new, y-new) = (0, 0)
let rng = gen-rng-f(seed1)
let v = ()
for i in range(n) {
(rng, v) = uniform-f(rng, low: -2.0, high: 2.0, size: 2)
(x-new, y-new) = (x - v.at(1), y - v.at(0))
let col = color.mix((blue.transparentize(20%), 1-i/n), (green.transparentize(20%), i/n))
line(stroke: (paint: col, cap: "round", thickness: 2pt),
(x, y), (x-new, y-new)
)
(x, y) = (x-new, y-new)
}
})
]
And this one is based on your unique browser fingerprint!
#align(center)[
#cetz.canvas(length: 12pt, {
import cetz.draw: *
let n = 20
let (x, y) = (0, 0)
let (x-new, y-new) = (0, 0)
let rng = gen-rng-f(seed2)
let v = ()
for i in range(n) {
(rng, v) = uniform-f(rng, low: -2.0, high: 2.0, size: 2)
(x-new, y-new) = (x - v.at(1), y - v.at(0))
let col = color.mix((red.transparentize(20%), 1-i/n), (yellow.transparentize(20%), i/n))
line(stroke: (paint: col, cap: "round", thickness: 2pt),
(x, y), (x-new, y-new)
)
(x, y) = (x-new, y-new)
}
})
]
= A word from our sponsor
#lorem(900)

90
example/lib.typ Normal file
View File

@@ -0,0 +1,90 @@
#let request = json(bytes(sys.inputs.at("request")))
#import "@preview/charged-ieee:0.1.4": ieee
#let parse-rfc3339-datetime(rfc3339-string) = {
// Split into date and time+offset
let (date-part, time-and-offset-part) = rfc3339-string.split("T")
// Parse date
let (year, month, day) = date-part.split("-").map(int)
// Handle UTC "Z" or offset (+/-hh:mm)
let time-part = time-and-offset-part
let offset = none
if time-part.ends-with("Z") {
time-part = time-part.slice(0, time-part.len() - 1)
offset = (0, 0) // UTC
} else if time-part.contains("+") {
let parts = time-part.split("+")
time-part = parts.first()
offset = parts.last().split(":").map(int)
offset = (offset.at(0), offset.at(1)) // (+hh, +mm)
} else if time-part.contains("-") {
let idx = time-part.rfind("-")
time-part = time-part.slice(0, idx)
let offstr = time-part.slice(idx+1)
offset = offstr.split(":").map(int)
offset = (-offset.at(0), -offset.at(1)) // (-hh, -mm)
}
// Split time, possibly with fractional seconds
let time-parts = time-part.split(":")
let hour = int(time-parts.at(0))
let minute = int(time-parts.at(1))
let second = 0
if time-parts.len() > 2 {
if time-parts.at(2).contains(".") {
let sec-parts = time-parts.at(2).split(".")
second = int(sec-parts.first())
// Fractions are ignored, but you could round if needed
} else {
second = int(time-parts.at(2))
}
}
return datetime(
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
)
}
#let currentDateTime = parse-rfc3339-datetime(request.timestamp)
#let ua = request.headers.at("user-agent", default: "")
#let browser = if ua.contains("Firefox") {
"Firefox"
} else if ua.contains("Edg/") {
"Edge"
} else if ua.contains("Chrome/") {
"Chrome"
} else if ua.contains("Safari/6") or ua.contains("Safari/7") {
"Safari"
} else {
"Unknown"
}
#let platform = if ua.contains("Win64") {
"Windows"
} else if ua.contains("Android") {
"Android"
} else if ua.contains("iPhone OS") {
"iPhone"
} else {
"Unknown"
}
#let styling(it) = {
show link: this => {
underline(text(this, fill: rgb("#5555ff")))
}
it
}

14
example/page2.typ Normal file
View File

@@ -0,0 +1,14 @@
#import "lib.typ"
#show: lib.styling
#text(link("/")[Go back])
#set text(font: "Comic Sans MS", fill: rgb("#0000ff"))
= Hey this is a sub-page XD
#image("assets/nyan.jpg")
Go check out my #link("/hahahahhahagettrolled")[404 Page], oh! And the #link("/debug.typ")[debug page] while you are at it!

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module mikee.dev/typst-framework
go 1.22.4