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.QueryRaw = arg("q")
90 skunky.Query = url.QueryEscape(skunky.QueryRaw)
91 skunky.Page = p
92
93 if t := arg("type"); len(t) > 0 {
94 skunky.Type = rune(t[0])
95 }
96
97 if arg("atom") == "true" {
98 skunky.Atom = true
99 }
100
101 if CFG.Proxy {
102 w.Header().Add("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'")
103 } else {
104 w.Header().Add("Content-Security-Policy", "default-src 'self'; img-src 'self' *.wixmp.com; script-src 'none'; style-src 'self' 'unsafe-inline'")
105 }
106
107 w.Header().Add("X-Frame-Options", "DENY")
108
109 switch skunky.Endpoint {
110 // main
111 case "":
112 skunky.ExecuteTemplate("index.htm", "html", &CFG.URI)
113 case "about":
114 skunky.Templates.About = About
115 skunky.ExecuteTemplate("about.htm", "html", &skunky)
116 case "post":
117 skunky.Deviation(path[2], path[3])
118 case "search":
119 skunky.Search()
120 case "dd":
121 skunky.DD()
122 case "group_user":
123 skunky.GRUser()
124
125 // media
126 case "media":
127 switch path[2] {
128 case "file":
129 if a := arg("filename"); a != "" {
130 skunky.SetFilename(a)
131 }
132 skunky.DownloadAndSendMedia(path[3], next(path, 4))
133 case "emojitar":
134 skunky.Emojitar(path[3])
135 }
136 case "stylesheet":
137 w.Header().Add("Content-Type", "text/css")
138 _, _ = w.Write(open("css/skunky.css"))
139 case "favicon.ico":
140 _, _ = w.Write(open("images/logo.png"))
141
142 // API
143 case "api":
144 w.Header().Add("Content-Type", "application/json")
145 switch path[2] {
146 case "instance":
147 skunky.API.Info()
148 case "random":
149 skunky.API.Random()
150 default:
151 skunky.API.Error("Not Found", 404)
152 }
153
154 // 404
155 default:
156 skunky.ReturnHTTPError(404)
157 }
158 }
159
160 http.HandleFunc("/", handle)
161 println("SkunkyArt is listening on", CFG.Listen)
162
163 // Explicit timeouts: the bare http.ListenAndServe has none, so a slow client
164 // can hold a connection (and its handler) open indefinitely. WriteTimeout is
165 // generous because media proxying streams large files through a handler.
166 srv := &http.Server{
167 Addr: CFG.Listen,
168 ReadHeaderTimeout: 10 * time.Second,
169 ReadTimeout: 30 * time.Second,
170 WriteTimeout: 120 * time.Second,
171 IdleTimeout: 120 * time.Second,
172 }
173 tryWithExitStatus(srv.ListenAndServe(), 1)
174}