Initial Commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
# Build output
|
||||
main.exe
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
134
cmd/main.go
Normal 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)
|
||||
}
|
||||
9
docker-compose.example.yml
Normal file
9
docker-compose.example.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
|
||||
services:
|
||||
typstframework:
|
||||
build: .
|
||||
ports:
|
||||
- "5082:8080"
|
||||
volumes:
|
||||
- ./example:/var/www/typst
|
||||
58
example/404.typ
Normal file
58
example/404.typ
Normal 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
BIN
example/assets/404.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
example/assets/nyan.jpg
Normal file
BIN
example/assets/nyan.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
4
example/debug.typ
Normal file
4
example/debug.typ
Normal file
@@ -0,0 +1,4 @@
|
||||
#import "lib.typ"
|
||||
|
||||
#lib.request
|
||||
|
||||
0
example/fonts/.gitkeep
Normal file
0
example/fonts/.gitkeep
Normal file
131
example/index.typ
Normal file
131
example/index.typ
Normal 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
90
example/lib.typ
Normal 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
14
example/page2.typ
Normal 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!
|
||||
Reference in New Issue
Block a user