keiji-ctl.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "flag"
  6. "fmt"
  7. "io"
  8. "log"
  9. "mime/multipart"
  10. "net/http"
  11. "net/url"
  12. "os"
  13. "path"
  14. "path/filepath"
  15. "strings"
  16. "git.aetherial.dev/aeth/keiji/pkg/auth"
  17. "git.aetherial.dev/aeth/keiji/pkg/controller"
  18. "git.aetherial.dev/aeth/keiji/pkg/storage"
  19. _ "github.com/mattn/go-sqlite3"
  20. )
  21. // newRequest sends an Origin matching the target for unsafe requests. Never use
  22. // a caller-provided Origin or include URL credentials, paths, queries or fragments.
  23. func newRequest(method, target string, body io.Reader) (*http.Request, error) {
  24. req, err := http.NewRequest(method, target, body)
  25. if err != nil {
  26. return nil, err
  27. }
  28. u := req.URL
  29. if u.Opaque != "" || u.User != nil || u.Hostname() == "" ||
  30. (!strings.EqualFold(u.Scheme, "http") && !strings.EqualFold(u.Scheme, "https")) {
  31. return nil, fmt.Errorf("invalid HTTP target URL: %q", target)
  32. }
  33. switch method {
  34. case http.MethodGet, http.MethodHead, http.MethodOptions:
  35. default:
  36. req.Header.Set("Origin", strings.ToLower(u.Scheme)+"://"+u.Host)
  37. }
  38. return req, nil
  39. }
  40. // Treat redirects as errors at the call site. In particular, do not replay
  41. // credentials, cookies or mutation bodies at a redirect destination.
  42. func newHTTPClient() *http.Client {
  43. return &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
  44. return http.ErrUseLastResponse
  45. }}
  46. }
  47. func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) {
  48. file, err := os.Open(path)
  49. if err != nil {
  50. return nil, err
  51. }
  52. defer file.Close()
  53. body := &bytes.Buffer{}
  54. writer := multipart.NewWriter(body)
  55. part, err := writer.CreateFormFile(paramName, filepath.Base(path))
  56. if err != nil {
  57. return nil, err
  58. }
  59. _, err = io.Copy(part, file)
  60. if err != nil {
  61. log.Fatal(err)
  62. }
  63. for key, val := range params {
  64. _ = writer.WriteField(key, val)
  65. }
  66. err = writer.Close()
  67. if err != nil {
  68. return nil, err
  69. }
  70. req, err := newRequest(http.MethodPost, uri, body)
  71. if err != nil {
  72. return nil, err
  73. }
  74. req.Header.Set("Content-Type", writer.FormDataContentType())
  75. return req, err
  76. }
  77. // authenticate and get the cookie needed to make updates
  78. func authenticate(url, username, password string) *http.Cookie {
  79. client := newHTTPClient()
  80. b, _ := json.Marshal(auth.Credentials{Username: username, Password: password})
  81. req, err := newRequest(http.MethodPost, url, bytes.NewReader(b))
  82. if err != nil {
  83. log.Fatal("auth failed: ", err)
  84. }
  85. req.Header.Add("Content-Type", "application/json")
  86. resp, err := client.Do(req)
  87. if err != nil {
  88. log.Fatal("auth failed: ", err)
  89. }
  90. defer resp.Body.Close()
  91. if resp.StatusCode > 200 {
  92. msg, _ := io.ReadAll(resp.Body)
  93. log.Fatal("Invalid credentials or server error: ", string(msg), "\n Status code: ", resp.StatusCode)
  94. }
  95. cookies := resp.Cookies()
  96. for i := range cookies {
  97. if cookies[i].Name == controller.AUTH_COOKIE_NAME {
  98. return cookies[i]
  99. }
  100. }
  101. log.Fatal("Auth cookie not found.")
  102. return nil
  103. }
  104. // prepare the auth cookie
  105. func prepareCookie(address string) *http.Cookie {
  106. parsedAddr, err := url.Parse(address)
  107. if err != nil {
  108. log.Fatal("unparseable address: ", address, " error: ", err)
  109. }
  110. dn := parsedAddr.Hostname()
  111. var preparedCookie *http.Cookie
  112. if cookie == "" {
  113. log.Fatal("Cookie cannot be empty.")
  114. } else {
  115. preparedCookie = &http.Cookie{Value: cookie, Name: controller.AUTH_COOKIE_NAME, Domain: dn}
  116. }
  117. return preparedCookie
  118. }
  119. var pngFile string
  120. var redirect string
  121. var text string
  122. var col string
  123. var cmd string
  124. var address string
  125. var cookie string
  126. func main() {
  127. flag.StringVar(&pngFile, "png", "", "The location of the PNG to upload")
  128. flag.StringVar(&redirect, "redirect", "", "the website that the navbar will redirect to")
  129. flag.StringVar(&text, "text", "", "the text to display on the menu item")
  130. flag.StringVar(&col, "col", "", "the column to add/populate the admin table item under")
  131. flag.StringVar(&cmd, "cmd", "", "the 'command' for the seed program to use, currently supports options 'admin', 'menu', and 'asset', 'nav'")
  132. flag.StringVar(&address, "address", "https://aetherial.dev", "override the url to contact.")
  133. flag.StringVar(&cookie, "cookie", "", "pass a cookie to bypass direct authentication")
  134. flag.Parse()
  135. client := newHTTPClient()
  136. switch cmd {
  137. case "auth":
  138. cookie := authenticate(fmt.Sprintf("%s/login", address), os.Getenv("KEIJI_USERNAME"), os.Getenv("KEIJI_PASSWORD"))
  139. fmt.Println(cookie.Value)
  140. case "asset":
  141. b, err := os.ReadFile(pngFile)
  142. if err != nil {
  143. log.Fatal(err)
  144. }
  145. _, fileName := path.Split(pngFile)
  146. item := storage.Asset{
  147. Name: fileName,
  148. Data: b,
  149. }
  150. data, _ := json.Marshal(item)
  151. req, err := newRequest(http.MethodPost, fmt.Sprintf("%s/admin/asset", address), bytes.NewReader(data))
  152. if err != nil {
  153. log.Fatal(err)
  154. }
  155. req.AddCookie(prepareCookie(address))
  156. req.Header.Add("Content-Type", "application/json")
  157. resp, err := client.Do(req)
  158. if err != nil {
  159. fmt.Println("There was an error performing the desired request: ", err.Error())
  160. os.Exit(1)
  161. }
  162. if resp.StatusCode > 200 {
  163. defer resp.Body.Close()
  164. b, _ := io.ReadAll(resp.Body)
  165. fmt.Println("There was an error performing the desired request: ", string(b))
  166. os.Exit(2)
  167. }
  168. fmt.Println("navigation bar item upload successfully.")
  169. os.Exit(0)
  170. case "nav":
  171. fmt.Println(string(pngFile))
  172. _, fileName := path.Split(pngFile)
  173. extraParams := map[string]string{
  174. "link": fileName,
  175. "redirect": redirect,
  176. }
  177. req, err := newfileUploadRequest(address+"/admin/navbar", extraParams, "file", pngFile)
  178. if err != nil {
  179. log.Fatal(err)
  180. }
  181. req.AddCookie(prepareCookie(address))
  182. resp, err := client.Do(req)
  183. if err != nil {
  184. fmt.Println("There was an error performing the desired request: ", err.Error())
  185. os.Exit(1)
  186. }
  187. if resp.StatusCode > 200 {
  188. defer resp.Body.Close()
  189. b, _ := io.ReadAll(resp.Body)
  190. fmt.Println("There was an error performing the desired request: ", string(b))
  191. os.Exit(2)
  192. }
  193. fmt.Println("png item upload successfully.")
  194. os.Exit(0)
  195. case "menu":
  196. b, _ := json.Marshal(storage.LinkPair{Text: text, Link: redirect})
  197. req, err := newRequest(http.MethodPost, fmt.Sprintf("%s/admin/menu", address), bytes.NewReader(b))
  198. if err != nil {
  199. log.Fatal(err)
  200. }
  201. req.AddCookie(prepareCookie(address))
  202. req.Header.Add("Content-Type", "application/json")
  203. resp, err := client.Do(req)
  204. if err != nil {
  205. fmt.Println("There was an error performing the desired request: ", err.Error())
  206. os.Exit(1)
  207. }
  208. if resp.StatusCode > 200 {
  209. defer resp.Body.Close()
  210. b, _ := io.ReadAll(resp.Body)
  211. fmt.Println("There was an error performing the desired request: ", string(b))
  212. os.Exit(3)
  213. }
  214. fmt.Println("menu item uploaded successfully.")
  215. os.Exit(0)
  216. case "admin":
  217. tables := make(map[string][]storage.TableData)
  218. adminPage := storage.AdminPage{Tables: tables}
  219. adminPage.Tables[col] = append(adminPage.Tables[col], storage.TableData{Link: redirect, DisplayName: text})
  220. b, _ := json.Marshal(adminPage)
  221. req, err := newRequest(http.MethodPost, fmt.Sprintf("%s/admin/panel", address), bytes.NewReader(b))
  222. if err != nil {
  223. log.Fatal(err)
  224. }
  225. req.AddCookie(prepareCookie(address))
  226. req.Header.Add("Content-Type", "application/json")
  227. resp, err := client.Do(req)
  228. if err != nil {
  229. fmt.Println("There was an error performing the desired request: ", err.Error())
  230. os.Exit(1)
  231. }
  232. if resp.StatusCode > 200 {
  233. defer resp.Body.Close()
  234. b, _ := io.ReadAll(resp.Body)
  235. fmt.Println("There was an error performing the desired request: ", string(b))
  236. os.Exit(4)
  237. }
  238. fmt.Println("admin item added successfully.")
  239. os.Exit(0)
  240. }
  241. }