http.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. // Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
  2. // Use of this source code is governed by the MIT-license that can be
  3. // found in the LICENSE file.
  4. package main
  5. import (
  6. "bufio"
  7. "bytes"
  8. "encoding/json"
  9. "fmt"
  10. "html/template"
  11. "io/ioutil"
  12. "log"
  13. "net"
  14. "net/http"
  15. "os"
  16. "path"
  17. "path/filepath"
  18. "sort"
  19. "strings"
  20. "time"
  21. "github.com/gorilla/mux"
  22. "github.com/gorilla/websocket"
  23. "github.com/microcosm-cc/bluemonday"
  24. "github.com/la5nta/wl2k-go/catalog"
  25. "github.com/la5nta/wl2k-go/fbb"
  26. "github.com/la5nta/wl2k-go/mailbox"
  27. "github.com/la5nta/pat/internal/gpsd"
  28. )
  29. // Status represents a status report as sent to the Web GUI
  30. type Status struct {
  31. ActiveListeners []string `json:"active_listeners"`
  32. Connected bool `json:"connected"`
  33. RemoteAddr string `json:"remote_addr"`
  34. HTTPClients []string `json:"http_clients"`
  35. }
  36. // Progress represents a progress report as sent to the Web GUI
  37. type Progress struct {
  38. BytesTransferred int `json:"bytes_transferred"`
  39. BytesTotal int `json:"bytes_total"`
  40. MID string `json:"mid"`
  41. Subject string `json:"subject"`
  42. Receiving bool `json:"receiving"`
  43. Sending bool `json:"sending"`
  44. Done bool `json:"done"`
  45. }
  46. // Notification represents a desktop notification as sent to the Web GUI
  47. type Notification struct {
  48. Title string `json:"title"`
  49. Body string `json:"body"`
  50. }
  51. var websocketHub *WSHub
  52. //go:generate go install -v ./vendor/github.com/jteeuwen/go-bindata/go-bindata ./vendor/github.com/elazarl/go-bindata-assetfs/go-bindata-assetfs
  53. //go:generate go-bindata-assetfs res/...
  54. func ListenAndServe(addr string) error {
  55. log.Printf("Starting HTTP service (%s)...", addr)
  56. if host, _, _ := net.SplitHostPort(addr); host == "" && config.GPSd.EnableHTTP {
  57. // TODO: maybe make a popup showing the warning ont the web UI?
  58. fmt.Fprintf(logWriter,"\nWARNING: You have enable GPSd HTTP endpoint (enable_http). You might expose" +
  59. "\n your current position to anyone who has access to the Pat web interface!\n\n")
  60. }
  61. r := mux.NewRouter()
  62. r.HandleFunc("/api/connect_aliases", connectAliasesHandler).Methods("GET")
  63. r.HandleFunc("/api/connect", ConnectHandler)
  64. r.HandleFunc("/api/mailbox/{box}", mailboxHandler).Methods("GET")
  65. r.HandleFunc("/api/mailbox/{box}/{mid}", messageHandler).Methods("GET")
  66. r.HandleFunc("/api/mailbox/{box}/{mid}", messageDeleteHandler).Methods("DELETE")
  67. r.HandleFunc("/api/mailbox/{box}/{mid}/{attachment}", attachmentHandler).Methods("GET")
  68. r.HandleFunc("/api/mailbox/{box}/{mid}/read", readHandler).Methods("POST")
  69. r.HandleFunc("/api/mailbox/{box}", postMessageHandler).Methods("POST")
  70. r.HandleFunc("/api/posreport", postPositionHandler).Methods("POST")
  71. r.HandleFunc("/api/status", statusHandler).Methods("GET")
  72. r.HandleFunc("/api/current_gps_position", positionHandler).Methods("GET")
  73. r.HandleFunc("/ws", wsHandler)
  74. r.HandleFunc("/ui", uiHandler).Methods("GET")
  75. r.HandleFunc("/", rootHandler).Methods("GET")
  76. http.Handle("/", r)
  77. http.Handle("/res/", http.StripPrefix("/res/", http.FileServer(assetFS())))
  78. websocketHub = NewWSHub()
  79. return http.ListenAndServe(addr, nil)
  80. }
  81. func rootHandler(w http.ResponseWriter, r *http.Request) {
  82. http.Redirect(w, r, "/ui", http.StatusFound)
  83. }
  84. func connectAliasesHandler(w http.ResponseWriter, r *http.Request) {
  85. json.NewEncoder(w).Encode(config.ConnectAliases)
  86. }
  87. func readHandler(w http.ResponseWriter, r *http.Request) {
  88. var data struct{ Read bool }
  89. if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
  90. http.Error(w, err.Error(), http.StatusBadRequest)
  91. log.Printf("%s %s: %s", r.Method, r.URL.Path, err)
  92. return
  93. }
  94. box, mid := mux.Vars(r)["box"], mux.Vars(r)["mid"]
  95. msg, err := mailbox.OpenMessage(path.Join(mbox.MBoxPath, box, mid+mailbox.Ext))
  96. if err != nil {
  97. http.Error(w, err.Error(), http.StatusInternalServerError)
  98. return
  99. }
  100. if err := mailbox.SetUnread(msg, !data.Read); err != nil {
  101. log.Printf("%s %s: %s", r.Method, r.URL.Path, err)
  102. http.Error(w, err.Error(), http.StatusInternalServerError)
  103. }
  104. }
  105. func postPositionHandler(w http.ResponseWriter, r *http.Request) {
  106. var pos catalog.PosReport
  107. if err := json.NewDecoder(r.Body).Decode(&pos); err != nil {
  108. http.Error(w, err.Error(), http.StatusBadRequest)
  109. return
  110. }
  111. r.Body.Close()
  112. if pos.Date.IsZero() {
  113. pos.Date = time.Now()
  114. }
  115. // Post to outbox
  116. msg := pos.Message(fOptions.MyCall)
  117. if err := mbox.AddOut(msg); err != nil {
  118. log.Println(err)
  119. http.Error(w, err.Error(), http.StatusInternalServerError)
  120. } else {
  121. fmt.Fprintln(w, "Position update posted")
  122. }
  123. }
  124. func isInPath(base string, path string) error {
  125. _, err := filepath.Rel(base, path)
  126. return err
  127. }
  128. func postMessageHandler(w http.ResponseWriter, r *http.Request) {
  129. box := mux.Vars(r)["box"]
  130. if box == "out" {
  131. postOutboundMessageHandler(w, r)
  132. return
  133. }
  134. srcPath := r.Header.Get("X-Pat-SourcePath")
  135. if srcPath == "" {
  136. http.Error(w, "Not implemented", http.StatusNotImplemented)
  137. return
  138. }
  139. srcPath = strings.TrimPrefix(srcPath, "/api/mailbox/")
  140. srcPath = filepath.Join(mbox.MBoxPath, srcPath+mailbox.Ext)
  141. // Check that we don't escape our mailbox path
  142. srcPath = filepath.Clean(srcPath)
  143. if err := isInPath(mbox.MBoxPath, srcPath); err != nil {
  144. log.Println("Malicious source path in move:", err)
  145. http.Error(w, err.Error(), http.StatusBadRequest)
  146. return
  147. }
  148. targetPath := filepath.Join(mbox.MBoxPath, box, filepath.Base(srcPath))
  149. if err := os.Rename(srcPath, targetPath); err != nil {
  150. log.Println("Could not move message:", err)
  151. http.Error(w, err.Error(), http.StatusBadRequest)
  152. } else {
  153. json.NewEncoder(w).Encode("OK")
  154. }
  155. }
  156. func postOutboundMessageHandler(w http.ResponseWriter, r *http.Request) {
  157. err := r.ParseMultipartForm(10 * (1024 ^ 2)) // 10Mb
  158. if err != nil {
  159. http.Error(w, err.Error(), http.StatusInternalServerError)
  160. return
  161. }
  162. m := r.MultipartForm
  163. msg := fbb.NewMessage(fbb.Private, fOptions.MyCall)
  164. // files
  165. files := m.File["files"]
  166. for _, f := range files {
  167. // For some unknown reason, we receive this empty unnamed file when no
  168. // attachment is provided. Prior to Go 1.10, this was filtered by
  169. // multipart.Reader.
  170. if isEmptyFormFile(f) {
  171. continue
  172. }
  173. if f.Filename == "" {
  174. http.Error(w, "Missing attachment name", http.StatusBadRequest)
  175. return
  176. }
  177. file, err := f.Open()
  178. if err != nil {
  179. http.Error(w, err.Error(), http.StatusInternalServerError)
  180. return
  181. }
  182. p, err := ioutil.ReadAll(file)
  183. file.Close()
  184. if err != nil {
  185. http.Error(w, err.Error(), http.StatusInternalServerError)
  186. return
  187. }
  188. if isImageMediaType(f.Filename, f.Header.Get("Content-Type")) {
  189. log.Printf("Auto converting '%s' [%s]...", f.Filename, f.Header.Get("Content-Type"))
  190. if converted, err := convertImage(bytes.NewReader(p)); err != nil {
  191. log.Printf("Error converting image: %s", err)
  192. } else {
  193. log.Printf("Done converting '%s'.", f.Filename)
  194. ext := path.Ext(f.Filename)
  195. f.Filename = f.Filename[:len(f.Filename)-len(ext)] + ".jpg"
  196. p = converted
  197. }
  198. }
  199. if err != nil {
  200. http.Error(w, err.Error(), http.StatusInternalServerError)
  201. return
  202. }
  203. msg.AddFile(fbb.NewFile(f.Filename, p))
  204. }
  205. // Other fields
  206. if v := m.Value["to"]; len(v) == 1 {
  207. addrs := strings.FieldsFunc(v[0], SplitFunc)
  208. msg.AddTo(addrs...)
  209. }
  210. if v := m.Value["cc"]; len(v) == 1 {
  211. addrs := strings.FieldsFunc(v[0], SplitFunc)
  212. msg.AddCc(addrs...)
  213. }
  214. if v := m.Value["subject"]; len(v) == 1 {
  215. msg.SetSubject(v[0])
  216. }
  217. if v := m.Value["body"]; len(v) == 1 {
  218. msg.SetBody(v[0])
  219. }
  220. if v := m.Value["p2ponly"]; len(v) == 1 && v[0] != "" {
  221. msg.Header.Set("X-P2POnly", "true")
  222. }
  223. if v := m.Value["date"]; len(v) == 1 {
  224. t, err := time.Parse(time.RFC3339, v[0])
  225. if err != nil {
  226. log.Printf("Unable to parse message date: %s", err)
  227. http.Error(w, err.Error(), http.StatusBadRequest)
  228. return
  229. }
  230. msg.SetDate(t)
  231. } else {
  232. log.Printf("Missing date value")
  233. http.Error(w, "Missing date value", http.StatusBadRequest)
  234. return
  235. }
  236. if err := msg.Validate(); err != nil {
  237. http.Error(w, "Validation error: "+err.Error(), http.StatusBadRequest)
  238. return
  239. }
  240. // Post to outbox
  241. if err := mbox.AddOut(msg); err != nil {
  242. log.Println(err)
  243. http.Error(w, err.Error(), http.StatusInternalServerError)
  244. return
  245. }
  246. w.WriteHeader(http.StatusCreated)
  247. var buf bytes.Buffer
  248. msg.Write(&buf)
  249. fmt.Fprintf(w, "Message posted (%.2f kB)", float64(buf.Len()/1024))
  250. }
  251. func wsHandler(w http.ResponseWriter, r *http.Request) {
  252. upgrader := websocket.Upgrader{
  253. ReadBufferSize: 1024,
  254. WriteBufferSize: 1024,
  255. }
  256. conn, err := upgrader.Upgrade(w, r, nil)
  257. if err != nil {
  258. log.Println(err)
  259. return
  260. }
  261. conn.WriteJSON(struct{ MyCall string }{fOptions.MyCall})
  262. websocketHub.Handle(conn)
  263. }
  264. func uiHandler(w http.ResponseWriter, r *http.Request) {
  265. data, err := Asset(path.Join("res", "tmpl", "index.html"))
  266. if err != nil {
  267. log.Fatal(err)
  268. }
  269. t := template.New("index.html") //create a new template
  270. t, err = t.Parse(string(data))
  271. if err != nil {
  272. log.Fatal(err)
  273. }
  274. tmplData := struct{ AppName, Version, Mycall string }{AppName, versionString(), fOptions.MyCall}
  275. err = t.Execute(w, tmplData)
  276. if err != nil {
  277. log.Fatal(err)
  278. }
  279. }
  280. func getStatus() Status {
  281. status := Status{
  282. ActiveListeners: []string{},
  283. Connected: exchangeConn != nil,
  284. HTTPClients: websocketHub.ClientAddrs(),
  285. }
  286. for _, tl := range listenHub.Active() {
  287. status.ActiveListeners = append(status.ActiveListeners, tl.Name())
  288. }
  289. sort.Strings(status.ActiveListeners)
  290. if exchangeConn != nil {
  291. addr := exchangeConn.RemoteAddr()
  292. status.RemoteAddr = fmt.Sprintf("%s:%s", addr.Network(), addr)
  293. }
  294. return status
  295. }
  296. func statusHandler(w http.ResponseWriter, req *http.Request) { json.NewEncoder(w).Encode(getStatus()) }
  297. func positionHandler(w http.ResponseWriter, req *http.Request) {
  298. // Throw error if GPSd http endpoint is not enabled
  299. if !config.GPSd.EnableHTTP || config.GPSd.Addr == "" {
  300. http.Error(w, "GPSd not enabled or address not set in config file", http.StatusInternalServerError)
  301. return
  302. }
  303. host, _, _ := net.SplitHostPort(req.RemoteAddr)
  304. log.Printf("Location data from GPSd served to %s", host)
  305. conn, err := gpsd.Dial(config.GPSd.Addr)
  306. if err != nil {
  307. // do not pass error message to response as GPSd address might be leaked
  308. http.Error(w, "GPSd Dial failed", http.StatusInternalServerError)
  309. return
  310. }
  311. defer conn.Close()
  312. conn.Watch(true)
  313. pos, err := conn.NextPosTimeout(5 * time.Second)
  314. if err != nil {
  315. http.Error(w, "GPSd get next position failed: " + err.Error(), http.StatusInternalServerError)
  316. return
  317. }
  318. if config.GPSd.UseServerTime {
  319. pos.Time = time.Now()
  320. }
  321. json.NewEncoder(w).Encode(pos)
  322. return
  323. }
  324. func ConnectHandler(w http.ResponseWriter, req *http.Request) {
  325. connectStr := req.FormValue("url")
  326. nMsgs := mbox.InboxCount()
  327. if success := Connect(connectStr); !success {
  328. http.Error(w, "Session failure", http.StatusInternalServerError)
  329. }
  330. json.NewEncoder(w).Encode(struct {
  331. NumReceived int
  332. }{
  333. mbox.InboxCount() - nMsgs,
  334. })
  335. }
  336. func mailboxHandler(w http.ResponseWriter, r *http.Request) {
  337. box := mux.Vars(r)["box"]
  338. var messages []*fbb.Message
  339. var err error
  340. switch box {
  341. case "in":
  342. messages, err = mbox.Inbox()
  343. case "out":
  344. messages, err = mbox.Outbox()
  345. case "sent":
  346. messages, err = mbox.Sent()
  347. case "archive":
  348. messages, err = mbox.Archive()
  349. default:
  350. http.NotFound(w, r)
  351. return
  352. }
  353. if err != nil {
  354. http.Error(w, err.Error(), http.StatusInternalServerError)
  355. log.Println(err)
  356. }
  357. sort.Sort(sort.Reverse(fbb.ByDate(messages)))
  358. jsonSlice := make([]JSONMessage, len(messages))
  359. for i, msg := range messages {
  360. jsonSlice[i] = JSONMessage{Message: msg}
  361. }
  362. json.NewEncoder(w).Encode(jsonSlice)
  363. return
  364. }
  365. type JSONMessage struct {
  366. *fbb.Message
  367. inclBody bool
  368. }
  369. func (m JSONMessage) MarshalJSON() ([]byte, error) {
  370. msg := struct {
  371. MID string
  372. Date time.Time
  373. From fbb.Address
  374. To []fbb.Address
  375. Cc []fbb.Address
  376. Subject string
  377. Body string
  378. BodyHTML string
  379. Files []*fbb.File
  380. P2POnly bool
  381. Unread bool
  382. }{
  383. MID: m.MID(),
  384. Date: m.Date(),
  385. From: m.From(),
  386. To: m.To(),
  387. Cc: m.Cc(),
  388. Subject: m.Subject(),
  389. Files: m.Files(),
  390. P2POnly: m.Header.Get("X-P2POnly") == "true",
  391. Unread: mailbox.IsUnread(m.Message),
  392. }
  393. if m.inclBody {
  394. msg.Body, _ = m.Body()
  395. unsafe := toHTML([]byte(msg.Body))
  396. msg.BodyHTML = string(bluemonday.UGCPolicy().SanitizeBytes(unsafe))
  397. }
  398. return json.Marshal(msg)
  399. }
  400. func messageDeleteHandler(w http.ResponseWriter, r *http.Request) {
  401. box, mid := mux.Vars(r)["box"], mux.Vars(r)["mid"]
  402. path := filepath.Clean(filepath.Join(mbox.MBoxPath, box, mid+mailbox.Ext))
  403. if err := isInPath(mbox.MBoxPath, path); err != nil {
  404. log.Println("Malicious source path in move:", err)
  405. http.Error(w, err.Error(), http.StatusBadRequest)
  406. return
  407. }
  408. err := os.Remove(path)
  409. if os.IsNotExist(err) {
  410. http.NotFound(w, r)
  411. return
  412. } else if err != nil {
  413. http.Error(w, err.Error(), http.StatusInternalServerError)
  414. }
  415. json.NewEncoder(w).Encode("OK")
  416. }
  417. func messageHandler(w http.ResponseWriter, r *http.Request) {
  418. box, mid := mux.Vars(r)["box"], mux.Vars(r)["mid"]
  419. msg, err := mailbox.OpenMessage(path.Join(mbox.MBoxPath, box, mid+mailbox.Ext))
  420. if os.IsNotExist(err) {
  421. http.NotFound(w, r)
  422. return
  423. } else if err != nil {
  424. log.Println(err)
  425. http.Error(w, err.Error(), http.StatusInternalServerError)
  426. return
  427. }
  428. json.NewEncoder(w).Encode(JSONMessage{msg, true})
  429. }
  430. func attachmentHandler(w http.ResponseWriter, r *http.Request) {
  431. // Attachments are potentially unsanitized HTML and/or javascript.
  432. // To avoid XSS, we enable the CSP sandbox directive so that these
  433. // attachments can't call other parts of the API (deny same origin).
  434. w.Header().Set("Content-Security-Policy", "sandbox allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts")
  435. // Allow different sandboxed attachments to refer to each other.
  436. // This can be useful to provide rich HTML content as attachments,
  437. // without having to bundle it all up in one big file.
  438. w.Header().Set("Access-Control-Allow-Origin", "null")
  439. box, mid, attachment := mux.Vars(r)["box"], mux.Vars(r)["mid"], mux.Vars(r)["attachment"]
  440. msg, err := mailbox.OpenMessage(path.Join(mbox.MBoxPath, box, mid+mailbox.Ext))
  441. if os.IsNotExist(err) {
  442. http.NotFound(w, r)
  443. return
  444. } else if err != nil {
  445. log.Println(err)
  446. http.Error(w, err.Error(), http.StatusInternalServerError)
  447. return
  448. }
  449. // Find and write attachment
  450. var found bool
  451. for _, f := range msg.Files() {
  452. if f.Name() != attachment {
  453. continue
  454. }
  455. found = true
  456. http.ServeContent(w, r, f.Name(), msg.Date(), bytes.NewReader(f.Data()))
  457. }
  458. if !found {
  459. http.NotFound(w, r)
  460. }
  461. }
  462. // toHTML takes the given body and turns it into proper html with
  463. // paragraphs, blockquote, and <br /> line breaks.
  464. func toHTML(body []byte) []byte {
  465. buf := bytes.NewBuffer(body)
  466. var out bytes.Buffer
  467. fmt.Fprint(&out, "<p>")
  468. scanner := bufio.NewScanner(buf)
  469. var blockquote int
  470. for scanner.Scan() {
  471. line := scanner.Text()
  472. if len(line) == 0 {
  473. fmt.Fprint(&out, "</p><p>")
  474. continue
  475. }
  476. depth := blockquoteDepth(line)
  477. for depth != blockquote {
  478. if depth > blockquote {
  479. fmt.Fprintf(&out, "</p><blockquote><p>")
  480. blockquote++
  481. } else {
  482. fmt.Fprintf(&out, "</p></blockquote><p>")
  483. blockquote--
  484. }
  485. }
  486. line = line[depth:]
  487. line = htmlEncode(line)
  488. line = linkify(line)
  489. fmt.Fprint(&out, line+"\n")
  490. }
  491. for ; blockquote > 0; blockquote-- {
  492. fmt.Fprintf(&out, "</p></blockquote>")
  493. }
  494. fmt.Fprint(&out, "</p>")
  495. return out.Bytes()
  496. }
  497. // blcokquoteDepth counts the number of '>' at the beginning of the string.
  498. func blockquoteDepth(str string) (n int) {
  499. for _, c := range str {
  500. if c != '>' {
  501. break
  502. }
  503. n++
  504. }
  505. return
  506. }
  507. // htmlEncode encodes html characters
  508. func htmlEncode(str string) string {
  509. str = strings.Replace(str, ">", "&gt;", -1)
  510. str = strings.Replace(str, "<", "&lt;", -1)
  511. return str
  512. }
  513. // linkify detects url's in the given string and adds <a href tag.
  514. //
  515. // It is recursive.
  516. func linkify(str string) string {
  517. start := strings.Index(str, "http")
  518. var needScheme bool
  519. if start < 0 {
  520. start = strings.Index(str, "www.")
  521. needScheme = true
  522. }
  523. if start < 0 {
  524. return str
  525. }
  526. end := strings.IndexAny(str[start:], " ,()[]")
  527. if end < 0 {
  528. end = len(str)
  529. } else {
  530. end += start
  531. }
  532. link := str[start:end]
  533. if needScheme {
  534. link = "http://" + link
  535. }
  536. return fmt.Sprintf(`%s<a href='%s' target='_blank'>%s</a>%s`, str[:start], link, str[start:end], linkify(str[end:]))
  537. }