krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
1package app
2
3import (
4 "io"
5 "net/http"
6 url "net/url"
7 "skunkyart/static"
8 "strconv"
9 "strings"
10 "time"
11)
12
13// Router registers the single catch-all handler that dispatches every path, then
14// serves until the process exits. It does not return on success.
15func Router() {
16 parsepath := func(path string) map[int]string {
17 if l := len(CFG.URI); len(path) > l {
18 path = path[l-1:]
19 } else {
20 path = "/"
21 }
22
23 parsedpath := make(map[int]string)
24 for x := 0; true; x++ {
25 slash := strings.Index(path, "/") + 1
26 content := path[:slash]
27 path = path[slash:]
28 if slash == 0 {
29 parsedpath[x] = path
30 break
31 }
32 parsedpath[x] = content[:slash-1]
33 }
34 return parsedpath
35 }
36
37 next := func(path map[int]string, from int) string {
38 var out strings.Builder
39 for x, l := from, len(path)-1; x <= l; x++ {
40 out.WriteString(path[x])
41 if x != l {
42 out.WriteString("/")
43 }
44 }
45 return out.String()
46 }
47
48 open := func(name string) []byte {
49 file, err := static.Templates.Open(name)
50 if err != nil {
51 try(err)
52 return nil
53 }
54 defer func() { try(file.Close()) }()
55
56 fileReaded, err := io.ReadAll(file)
57 if err != nil {
58 try(err)
59 return nil
60 }
61 return fileReaded
62 }
63
64 // the function that drives everything
65 handle := func(w http.ResponseWriter, r *http.Request) {
66 path := parsepath(r.URL.Path)
67
68 // Per-request, not a package global: requests arrive concurrently on
69 // different hosts and ports (bots hitting a proxy's alternate ports, for
70 // one), and a shared global lets one request's host leak into another's
71 // rendered URLs. Those URLs then point at a different origin, which this
72 // handler's own default-src 'self' CSP blocks.
73 host := "http://" + r.Host
74 if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
75 host = "https://" + r.Host
76 }
77
78 var skunky = skunkyart{Version: Release.Version, Host: host}
79 skunky._pth = r.URL.Path
80
81 skunky.Args = r.URL.Query()
82 arg := skunky.Args.Get
83 p, _ := strconv.Atoi(arg("p"))
84
85 skunky.Endpoint = path[1]
86 skunky.API.main = &skunky
87 skunky.Writer = w
88 skunky.BasePath = CFG.URI
89 skunky.Lang = ResolveLang(r.Header.Get("Accept-Language"))
90 skunky.QueryRaw = arg("q")
91 skunky.Query = url.QueryEscape(skunky.QueryRaw)
92 skunky.Page = p
93
94 if t := arg("type"); len(t) > 0 {
95 skunky.Type = rune(t[0])
96 }
97
98 if arg("atom") == "true" {
99 skunky.Atom = true
100 }
101
102 if CFG.Proxy {
103 w.Header().Add("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'")
104 } else {
105 w.Header().Add("Content-Security-Policy", "default-src 'self'; img-src 'self' *.wixmp.com; script-src 'none'; style-src 'self' 'unsafe-inline'")
106 }
107
108 w.Header().Add("X-Frame-Options", "DENY")
109
110 switch skunky.Endpoint {
111 // main
112 case "":
113 skunky.ExecuteTemplate("index.htm", "html", &CFG.URI)
114 case "about":
115 skunky.Templates.About = About
116 skunky.ExecuteTemplate("about.htm", "html", &skunky)
117 case "post":
118 skunky.Deviation(path[2], path[3])
119 case "search":
120 skunky.Search()
121 case "dd":
122 skunky.DD()
123 case "group_user":
124 skunky.GRUser()
125
126 // media
127 case "media":
128 switch path[2] {
129 case "file":
130 if a := arg("filename"); a != "" {
131 skunky.SetFilename(a)
132 }
133 skunky.DownloadAndSendMedia(path[3], next(path, 4))
134 case "emojitar":
135 skunky.Emojitar(path[3])
136 }
137 case "stylesheet":
138 w.Header().Add("Content-Type", "text/css")
139 _, _ = w.Write(open("css/skunky.css"))
140 // "auto" is the stylesheet as written: dark, with a light palette
141 // behind prefers-color-scheme. Forcing a theme means re-declaring
142 // that palette unconditionally, which outranks the media query
143 // because it comes later with equal specificity.
144 if css := forcedThemeCSS(); css != "" {
145 _, _ = w.Write([]byte(css))
146 }
147 case "favicon.ico":
148 _, _ = w.Write(open("images/logo.png"))
149
150 // API
151 case "api":
152 w.Header().Add("Content-Type", "application/json")
153 switch path[2] {
154 case "instance":
155 skunky.API.Info()
156 case "random":
157 skunky.API.Random()
158 case "search":
159 skunky.API.Search()
160 case "post":
161 skunky.API.Post(path[3], path[4])
162 default:
163 skunky.API.Error("Not Found", 404)
164 }
165
166 // 404
167 default:
168 skunky.ReturnHTTPError(404)
169 }
170 }
171
172 http.HandleFunc("/", handle)
173 println("SkunkyArt is listening on", CFG.Listen)
174
175 // Explicit timeouts: the bare http.ListenAndServe has none, so a slow client
176 // can hold a connection (and its handler) open indefinitely. WriteTimeout is
177 // generous because media proxying streams large files through a handler.
178 srv := &http.Server{
179 Addr: CFG.Listen,
180 ReadHeaderTimeout: 10 * time.Second,
181 ReadTimeout: 30 * time.Second,
182 WriteTimeout: 120 * time.Second,
183 IdleTimeout: 120 * time.Second,
184 }
185 tryWithExitStatus(srv.ListenAndServe(), 1)
186}