a cloud service to enable your own web server (owned by you and running on your computer) to be accessible on the internet in seconds, no credit card required
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1093 lines
36 KiB

4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
  1. package main
  2. import (
  3. "bytes"
  4. "crypto/hmac"
  5. "crypto/md5"
  6. "crypto/rand"
  7. "crypto/sha256"
  8. "encoding/base64"
  9. "encoding/binary"
  10. "encoding/hex"
  11. "encoding/json"
  12. "fmt"
  13. "html/template"
  14. "io"
  15. "io/ioutil"
  16. "log"
  17. "math"
  18. mathRand "math/rand"
  19. "net/http"
  20. "path/filepath"
  21. "regexp"
  22. "sort"
  23. "strconv"
  24. "strings"
  25. "sync"
  26. "time"
  27. "github.com/gorilla/mux"
  28. base58 "github.com/shengdoushi/base58"
  29. chart "github.com/wcharczuk/go-chart/v2"
  30. chartdrawing "github.com/wcharczuk/go-chart/v2/drawing"
  31. )
  32. type Session struct {
  33. SessionId string
  34. TenantId int
  35. Email string
  36. EmailVerified bool
  37. LaxCookie bool
  38. Expires time.Time
  39. Flash *map[string]string
  40. }
  41. type FrontendApp struct {
  42. Port int
  43. TLSCertificate string
  44. TLSKey string
  45. Domain string
  46. WorkingDirectory string
  47. Router *mux.Router
  48. EmailService *EmailService
  49. Model *DBModel
  50. Backend *BackendApp
  51. HTMLTemplates map[string]*template.Template
  52. PasswordHashSalt string
  53. SessionCache map[string]*Session
  54. SessionIdByTenantId map[int]string
  55. SessionCacheMutex *sync.Mutex
  56. basicURLPathRegex *regexp.Regexp
  57. AdminTenantId int
  58. }
  59. func initFrontend(workingDirectory string, config *Config, model *DBModel, backend *BackendApp, emailService *EmailService) FrontendApp {
  60. app := FrontendApp{
  61. Port: config.FrontendPort,
  62. TLSCertificate: config.FrontendTLSCertificate,
  63. TLSKey: config.FrontendTLSKey,
  64. Domain: config.FrontendDomain,
  65. WorkingDirectory: workingDirectory,
  66. Router: mux.NewRouter(),
  67. EmailService: emailService,
  68. Model: model,
  69. Backend: backend,
  70. HTMLTemplates: map[string]*template.Template{},
  71. PasswordHashSalt: "Ko0jOdSCzEyDtK4rmoocfcR9LxwOrIZsaVPBjImkb6AhRW6yNSmgsU122ArU1URBjcJ1EnskZ5r7",
  72. SessionCache: map[string]*Session{},
  73. SessionIdByTenantId: map[int]string{},
  74. SessionCacheMutex: &sync.Mutex{},
  75. basicURLPathRegex: regexp.MustCompile("(?i)[a-z0-9/?&_+-]+"),
  76. }
  77. app.handleWithSessionNotRequired("/", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  78. pageContent, err := app.renderTemplateToHTML("index.html", nil)
  79. if err != nil {
  80. app.unhandledError(responseWriter, err)
  81. return
  82. }
  83. highlightContent, err := app.renderTemplateToHTML("index-highlight.html", nil)
  84. if err != nil {
  85. app.unhandledError(responseWriter, err)
  86. return
  87. }
  88. app.buildPage(responseWriter, session, highlightContent, pageContent)
  89. })
  90. app.handleWithSessionNotRequired("/login", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  91. if request.Method == "POST" {
  92. remoteUsersHMAC := request.PostFormValue("hmac")
  93. loginSuccess := false
  94. tenantId, databasesHashedPassword, emailVerified := app.Model.GetLoginInfo(request.PostFormValue("email"))
  95. if remoteUsersHMAC == "" {
  96. saltedPassword := fmt.Sprintf("%s%s", app.PasswordHashSalt, request.PostFormValue("password"))
  97. hashedPasswordByteArray := sha256.Sum256([]byte(saltedPassword))
  98. remoteUsersHashedPassword := base64.StdEncoding.EncodeToString(hashedPasswordByteArray[:])
  99. loginSuccess = (remoteUsersHashedPassword == databasesHashedPassword)
  100. } else {
  101. timestampString := request.PostFormValue("timestamp")
  102. timestamp, err := strconv.ParseInt(timestampString, 10, 64)
  103. elapsedSeconds := time.Now().Unix() - timestamp
  104. if err != nil || elapsedSeconds < 0 || elapsedSeconds > 5*60 {
  105. (*session.Flash)["error"] += "Invalid timestamp\n"
  106. } else {
  107. databasesHashedPasswordBytes, err := base64.StdEncoding.DecodeString(databasesHashedPassword)
  108. if err != nil {
  109. app.unhandledError(responseWriter, err)
  110. return
  111. }
  112. hmacFromDB := hmac.New(sha256.New, databasesHashedPasswordBytes)
  113. hmacFromDB.Write([]byte(timestampString))
  114. hmacFromDBResultBytes := hmacFromDB.Sum(nil)
  115. loginSuccess = (remoteUsersHMAC == base64.StdEncoding.EncodeToString(hmacFromDBResultBytes))
  116. }
  117. }
  118. if loginSuccess {
  119. err := app.setSession(
  120. responseWriter,
  121. &Session{
  122. TenantId: tenantId,
  123. Email: strings.ToLower(request.PostFormValue("email")),
  124. EmailVerified: emailVerified,
  125. // we will use the SameSite Lax cookie policy until the first time that the user re-logs-in AFTER confirming thier
  126. // email address. After that we will use the SameSite Strict cookie policy
  127. // this is a "have your cake and eat it too" heuristic solution to
  128. // https://security.stackexchange.com/questions/220292/preventing-csrf-with-samesite-strict-without-degrading-user-experience
  129. // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute
  130. LaxCookie: !emailVerified,
  131. Expires: time.Now().Add(time.Hour),
  132. Flash: &(map[string]string{}),
  133. },
  134. )
  135. if err != nil {
  136. app.unhandledError(responseWriter, err)
  137. } else {
  138. returnTo := "/profile"
  139. if request.PostFormValue("returnTo") != "" && app.basicURLPathRegex.MatchString(request.PostFormValue("returnTo")) {
  140. returnTo = request.PostFormValue("returnTo")
  141. }
  142. http.Redirect(responseWriter, request, returnTo, http.StatusFound)
  143. }
  144. return
  145. } // else
  146. (*session.Flash)["error"] += "Invalid login credentials. Passwords are case-sensitive\n"
  147. }
  148. returnTo := ""
  149. if app.basicURLPathRegex.MatchString((*session.Flash)["returnTo"]) {
  150. returnTo = (*session.Flash)["returnTo"]
  151. }
  152. data := struct {
  153. PasswordHashSalt string
  154. Timestamp string
  155. ReturnTo string
  156. }{
  157. PasswordHashSalt: app.PasswordHashSalt,
  158. Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
  159. ReturnTo: returnTo,
  160. }
  161. app.buildPageFromTemplate(responseWriter, session, "login.html", data)
  162. })
  163. app.handleWithSessionNotRequired("/register", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  164. data := struct {
  165. Email string
  166. PasswordHashSalt string
  167. }{
  168. PasswordHashSalt: app.PasswordHashSalt,
  169. }
  170. if request.Method == "POST" {
  171. data.Email = request.PostFormValue("email")
  172. emailError := emailService.ValidateEmailAddress(data.Email)
  173. if emailError != "" {
  174. (*session.Flash)["error"] += fmt.Sprintln(emailError)
  175. }
  176. hashedPassword := request.PostFormValue("hashedPassword")
  177. if hashedPassword == "" {
  178. password := request.PostFormValue("password")
  179. if len(password) < 6 {
  180. (*session.Flash)["error"] += "password must be at least 6 characters\n"
  181. }
  182. saltedPassword := fmt.Sprintf("%s%s", app.PasswordHashSalt, password)
  183. hashedPasswordByteArray := sha256.Sum256([]byte(saltedPassword))
  184. hashedPassword = base64.StdEncoding.EncodeToString(hashedPasswordByteArray[:])
  185. }
  186. if (*session.Flash)["error"] == "" {
  187. tenantId, err := app.Model.Register(data.Email, hashedPassword)
  188. if err != nil {
  189. (*session.Flash)["error"] += fmt.Sprintln(err)
  190. } else {
  191. emailVerificationTokenBuffer := make([]byte, 4)
  192. rand.Read(emailVerificationTokenBuffer)
  193. emailVerificationToken := hex.EncodeToString(emailVerificationTokenBuffer)
  194. err := app.Model.CreateEmailVerificationToken(emailVerificationToken, tenantId, time.Now().Add(time.Hour))
  195. if err != nil {
  196. app.unhandledError(responseWriter, err)
  197. return
  198. }
  199. err = app.setSession(
  200. responseWriter,
  201. &Session{
  202. TenantId: tenantId,
  203. Email: strings.ToLower(data.Email),
  204. LaxCookie: true,
  205. Expires: time.Now().Add(time.Hour),
  206. Flash: &(map[string]string{}),
  207. },
  208. )
  209. if err != nil {
  210. app.unhandledError(responseWriter, err)
  211. return
  212. }
  213. protocol := "https"
  214. if app.Domain == "localhost" {
  215. protocol = "http"
  216. }
  217. specialPort := ""
  218. if app.Port != 80 && app.Port != 443 {
  219. specialPort = fmt.Sprintf(":%d", app.Port)
  220. }
  221. err = emailService.SendEmail(
  222. fmt.Sprintf("Please verify your account on %s", app.Domain),
  223. data.Email,
  224. fmt.Sprintf(
  225. "Please click the following link to verify your account: %s://%s%s/verify-email/%s",
  226. protocol, app.Domain, specialPort, emailVerificationToken,
  227. ),
  228. )
  229. if err != nil {
  230. (*session.Flash)["error"] += fmt.Sprintln(err)
  231. } else {
  232. http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
  233. return
  234. }
  235. }
  236. }
  237. }
  238. app.buildPageFromTemplate(responseWriter, session, "register.html", data)
  239. })
  240. verifyEmailHandler := func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  241. if session.EmailVerified {
  242. app.setFlash(responseWriter, session, "info", "Your email address has already been verified")
  243. http.Redirect(responseWriter, request, "/profile", http.StatusFound)
  244. return
  245. }
  246. log.Printf("verify email handler: [%s] [%d]\n", mux.Vars(request)["token"], session.TenantId)
  247. if mux.Vars(request)["token"] != "" && session.TenantId != 0 {
  248. err := app.Model.VerifyEmail(mux.Vars(request)["token"], session.TenantId)
  249. if err != nil {
  250. (*session.Flash)["error"] += fmt.Sprintln(err)
  251. } else {
  252. err = app.Backend.InitializeTenant(session.TenantId, session.Email)
  253. if err != nil {
  254. app.unhandledError(responseWriter, err)
  255. return
  256. }
  257. session.EmailVerified = true
  258. newSession := &Session{
  259. TenantId: session.TenantId,
  260. Email: session.Email,
  261. EmailVerified: true,
  262. LaxCookie: true,
  263. Expires: time.Now().Add(time.Hour),
  264. Flash: &(map[string]string{}),
  265. }
  266. err = app.setSession(responseWriter, newSession)
  267. if err != nil {
  268. app.unhandledError(responseWriter, err)
  269. return
  270. }
  271. app.setFlash(responseWriter, session, "info", "Your email address has been verified!")
  272. http.Redirect(responseWriter, request, "/profile", http.StatusFound)
  273. return
  274. }
  275. }
  276. app.buildPageFromTemplate(responseWriter, session, "verify-email.html", nil)
  277. }
  278. app.handleWithSession("/verify-email", verifyEmailHandler)
  279. app.handleWithSession("/verify-email/{token}", verifyEmailHandler)
  280. app.handleWithSession("/profile", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  281. if !session.EmailVerified {
  282. // anti-XSS: only set path into the flash cookie if it matches a basic url pattern
  283. if app.basicURLPathRegex.MatchString(request.URL.Path) {
  284. msg := fmt.Sprintf("Please verify your email address in order to access %s%s", app.Domain, request.URL.Path)
  285. app.setFlash(responseWriter, session, "info", msg)
  286. }
  287. http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
  288. return
  289. }
  290. rawHash := sha256.Sum256([]byte(session.SessionId))
  291. hashOfSessionId := fmt.Sprintf("%x", rawHash[:8])
  292. tenant, err := app.Model.GetTenant(session.TenantId)
  293. if err != nil {
  294. app.unhandledError(responseWriter, err)
  295. return
  296. }
  297. billingYear, billingMonth, _, _, _ := getBillingTimeInfo()
  298. usageTotal, err := app.Model.GetTenantUsageTotal(session.TenantId, billingYear, billingMonth)
  299. if err != nil {
  300. app.unhandledError(responseWriter, err)
  301. return
  302. }
  303. apiToken := (*session.Flash)["api-token"]
  304. if request.Method == "POST" {
  305. postedHashOfSessionId := request.PostFormValue("hashOfSessionId")
  306. if postedHashOfSessionId != hashOfSessionId {
  307. app.setFlash(responseWriter, session, "error", "anti-CSRF validation failed\n")
  308. http.Redirect(responseWriter, request, "/profile", http.StatusFound)
  309. return
  310. }
  311. action := request.PostFormValue("action")
  312. if action == "reload_api_token" {
  313. apiTokenBuffer := make([]byte, 16)
  314. rand.Read(apiTokenBuffer)
  315. apiToken := base58.Encode(apiTokenBuffer, base58.BitcoinAlphabet)
  316. rawHash := sha256.Sum256([]byte(apiToken))
  317. hashedAPIToken := fmt.Sprintf("%x", rawHash)
  318. err := app.Model.SetHashedAPIToken(session.TenantId, hashedAPIToken)
  319. if err != nil {
  320. app.unhandledError(responseWriter, err)
  321. return
  322. }
  323. app.setFlash(responseWriter, session, "api-token", apiToken)
  324. app.setFlash(responseWriter, session, "info", fmt.Sprintf("Success! Your new API Token is %s. It will not be displayed again, so make sure to copy and paste it or write it down now!\n", apiToken))
  325. } else {
  326. app.setFlash(responseWriter, session, "error", "unknown action\n")
  327. }
  328. http.Redirect(responseWriter, request, "/profile", http.StatusFound)
  329. return
  330. }
  331. data := struct {
  332. Subdomain string
  333. APIToken string
  334. BytesSoFar string
  335. BillingAlarmSMS string
  336. BillingAlarmEmail string
  337. BillingAlarmThreshold string
  338. BillingLimit string
  339. HashOfSessionId string
  340. }{
  341. Subdomain: tenant.Subdomain,
  342. APIToken: apiToken,
  343. BytesSoFar: ByteCountSI(usageTotal),
  344. BillingAlarmSMS: tenant.SMSAlarmNumber,
  345. BillingAlarmEmail: tenant.Email,
  346. BillingAlarmThreshold: fmt.Sprintf("%.2f", float64(tenant.BillingAlarmCents)/float64(100)),
  347. BillingLimit: fmt.Sprintf("%.2f", float64(tenant.ServiceLimitCents)/float64(100)),
  348. HashOfSessionId: hashOfSessionId,
  349. }
  350. app.buildPageFromTemplate(responseWriter, session, "profile.html", data)
  351. })
  352. app.handleWithSession("/profile/usage_graph.png", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  353. // tenant, err := app.Model.GetTenant(session.TenantId)
  354. // if err != nil {
  355. // app.unhandledError(responseWriter, err)
  356. // return
  357. // }
  358. _, _, start, end, _ := getBillingTimeInfo()
  359. usageMetrics, err := app.Model.GetTenantUsageMetrics(session.TenantId, start, end)
  360. if err != nil {
  361. app.unhandledError(responseWriter, err)
  362. return
  363. }
  364. type datapoint struct {
  365. Time time.Time
  366. Bytes int64
  367. }
  368. i := 0
  369. usageMetricObjects := make([]datapoint, len(usageMetrics))
  370. for time, bytez := range usageMetrics {
  371. usageMetricObjects[i] = datapoint{Time: time, Bytes: bytez}
  372. i++
  373. }
  374. sort.Slice(usageMetricObjects, func(i, j int) bool {
  375. return usageMetricObjects[j].Time.After(usageMetricObjects[i].Time)
  376. })
  377. xValues := make([]float64, len(usageMetricObjects)+3)
  378. yValues := make([]float64, len(usageMetricObjects)+3)
  379. var total int64 = 0
  380. xValues[0] = float64(start.UnixNano())
  381. yValues[0] = float64(0)
  382. xValues[1] = float64(usageMetricObjects[0].Time.UnixNano() - int64(time.Second*10))
  383. yValues[1] = float64(0)
  384. for i, obj := range usageMetricObjects {
  385. total = total + obj.Bytes
  386. xValues[i+2] = float64(obj.Time.UnixNano())
  387. yValues[i+2] = float64(total)
  388. }
  389. xValues[len(xValues)-1] = float64(end.UnixNano())
  390. yValues[len(yValues)-1] = float64(total)
  391. // it will err if the y scale is from 0 to 0
  392. if total == 0 {
  393. total = 1000
  394. }
  395. graph := chart.Chart{
  396. Width: 400,
  397. Height: 200,
  398. XAxis: chart.XAxis{
  399. ValueFormatter: func(v interface{}) string {
  400. if typed, isTyped := v.(float64); isTyped {
  401. timeInstance := time.Unix(0, int64(typed)).UTC()
  402. if timeInstance.After(end) {
  403. timeInstance = end
  404. }
  405. return timeInstance.Format("Jan 2")
  406. }
  407. return ""
  408. },
  409. Range: &chart.ContinuousRange{
  410. Min: float64(start.UnixNano()),
  411. Max: float64(end.UnixNano()),
  412. },
  413. },
  414. YAxis: chart.YAxis{
  415. ValueFormatter: func(v interface{}) string {
  416. if typed, isTyped := v.(float64); isTyped {
  417. return ByteCountSI(int64(typed))
  418. }
  419. return ""
  420. },
  421. Range: &chart.ContinuousRange{
  422. Min: float64(0),
  423. Max: float64(total) * float64(1.2),
  424. },
  425. },
  426. Series: []chart.Series{
  427. chart.ContinuousSeries{
  428. Name: "Bytes",
  429. Style: chart.Style{
  430. FillColor: chartdrawing.Color{R: 0, G: 100, B: 255, A: 70},
  431. },
  432. YAxis: chart.YAxisPrimary,
  433. XValues: xValues,
  434. YValues: yValues,
  435. },
  436. },
  437. }
  438. //log.Println(graph.YAxis.Range.GetDelta())
  439. buffer := bytes.NewBuffer([]byte{})
  440. err = graph.Render(chart.PNG, buffer)
  441. if err != nil {
  442. app.unhandledError(responseWriter, err)
  443. return
  444. }
  445. responseWriter.Header().Set("Content-Type", "image/png")
  446. responseWriter.Header().Set("Content-Length", strconv.Itoa(buffer.Len()))
  447. _, err = responseWriter.Write(buffer.Bytes())
  448. if err != nil {
  449. log.Printf("http Write error on usage_graph png: %+v", err)
  450. }
  451. })
  452. app.handleWithSessionNotRequired("/logout", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  453. err := app.Model.LogoutTenant(session.TenantId)
  454. if err != nil {
  455. app.unhandledError(responseWriter, err)
  456. return
  457. }
  458. app.deleteCookie(responseWriter, "sessionId")
  459. app.deleteCookie(responseWriter, "sessionIdLax")
  460. http.Redirect(responseWriter, request, "/", http.StatusFound)
  461. })
  462. app.handleWithSpecificUser("/admin", app.AdminTenantId, func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
  463. rawHash := sha256.Sum256([]byte(session.SessionId))
  464. hashOfSessionId := fmt.Sprintf("%x", rawHash[:8])
  465. if request.Method == "POST" {
  466. postedHashOfSessionId := request.PostFormValue("hashOfSessionId")
  467. log.Println(hashOfSessionId, postedHashOfSessionId)
  468. if postedHashOfSessionId != hashOfSessionId {
  469. app.setFlash(responseWriter, session, "error", "anti-CSRF validation failed\n")
  470. http.Redirect(responseWriter, request, "/admin", http.StatusFound)
  471. return
  472. }
  473. action := request.PostFormValue("action")
  474. if action == "delete-from-db" {
  475. instance := request.PostFormValue("instance")
  476. split := strings.Split(instance, "-")
  477. if len(split) != 2 {
  478. (*session.Flash)["error"] += fmt.Sprintf("invalid instance '%s'. expected <provider>-<provider_id>\n", instance)
  479. } else {
  480. err := app.Model.DeleteVPSInstance(split[0], split[1])
  481. if err != nil {
  482. app.unhandledError(responseWriter, err)
  483. return
  484. }
  485. }
  486. } else if action == "rebalance" {
  487. app.setFlash(responseWriter, session, "info", "rebalance has been kicked off in the background\n")
  488. go (func() {
  489. log.Println("Starting backendApp.Rebalance()")
  490. completed, err := app.Backend.Rebalance()
  491. if err != nil {
  492. log.Printf("Rebalance failed: %+v\n", err)
  493. } else if !completed {
  494. log.Println("Rebalance not complete yet. Running backendApp.Rebalance() again")
  495. _, err := app.Backend.Rebalance()
  496. if err != nil {
  497. log.Printf("Rebalance failed: %+v\n", err)
  498. }
  499. }
  500. })()
  501. } else if action == "configure_threshold_client" {
  502. err := app.Backend.WriteAdminTenantThresholdConfig()
  503. if err != nil {
  504. app.setFlash(responseWriter, session, "error", fmt.Sprintf("configure_threshold_client failed: %+v\n", err))
  505. log.Printf("configure_threshold_client failed: %+v\n", err)
  506. } else {
  507. app.setFlash(responseWriter, session, "info", "wrote threshold client config!\n")
  508. log.Println("wrote threshold client config!")
  509. }
  510. } else if action == "configure_threshold_server" {
  511. err := app.Backend.ConfigureAdminTenantOnThresholdServer()
  512. if err != nil {
  513. app.setFlash(responseWriter, session, "error", fmt.Sprintf("configure_threshold_server failed: %+v\n", err))
  514. log.Printf("configure_threshold_server failed: %+v\n", err)
  515. } else {
  516. app.setFlash(responseWriter, session, "info", "configured threshold server!\n")
  517. log.Println("configured threshold server!")
  518. }
  519. } else {
  520. app.setFlash(responseWriter, session, "error", fmt.Sprintf("Unknown action '%s'\n", action))
  521. }
  522. http.Redirect(responseWriter, request, "/admin", http.StatusFound)
  523. return
  524. }
  525. //TODO
  526. desiredInstancesPerTenant := 2
  527. tenantPinDuration := 6 * time.Hour
  528. billingYear, billingMonth, _, _, amountOfMonthElapsed := getBillingTimeInfo()
  529. validVpsInstances, dbOnlyInstances, cloudOnlyInstances, err := app.Backend.GetInstances()
  530. if err != nil {
  531. app.unhandledError(responseWriter, err)
  532. return
  533. }
  534. tenants, err := app.Model.GetTenants()
  535. if err != nil {
  536. app.unhandledError(responseWriter, err)
  537. return
  538. }
  539. tenantVpsInstanceRows, err := app.Model.GetTenantVPSInstanceRows(billingYear, billingMonth)
  540. if err != nil {
  541. app.unhandledError(responseWriter, err)
  542. return
  543. }
  544. // if you update the following loop, consider updating the similar one in backend.go 😬
  545. allocations := map[string]map[int]bool{}
  546. pinned := map[string]map[int]bool{}
  547. for _, row := range tenantVpsInstanceRows {
  548. vpsInstanceId := row.GetVPSInstanceId()
  549. vpsInstance, hasVPSInstance := validVpsInstances[vpsInstanceId]
  550. if hasVPSInstance {
  551. vpsInstance.Bytes += row.Bytes
  552. _, hasAllocationMap := allocations[vpsInstanceId]
  553. if !hasAllocationMap {
  554. allocations[vpsInstanceId] = map[int]bool{}
  555. pinned[vpsInstanceId] = map[int]bool{}
  556. }
  557. if row.Active {
  558. allocations[vpsInstanceId][row.TenantId] = true
  559. }
  560. if row.DeactivatedAt != nil && row.DeactivatedAt.Add(tenantPinDuration).After(time.Now()) {
  561. pinned[vpsInstanceId][row.TenantId] = true
  562. }
  563. }
  564. }
  565. healthStatus := app.Backend.HealthcheckInstances(validVpsInstances)
  566. type TenantDisplay struct {
  567. Name string
  568. Color template.CSS
  569. }
  570. type Thermometer struct {
  571. Height1 int
  572. Color1 template.CSS
  573. Height2 int
  574. Color2 template.CSS
  575. }
  576. type VPSInstanceForDisplay struct {
  577. VPSInstance
  578. CurrentTenants []TenantDisplay
  579. PinnedTenants []TenantDisplay
  580. ThermometerReal Thermometer
  581. ThermometerProjected Thermometer
  582. Healthy string
  583. }
  584. display := map[string]*VPSInstanceForDisplay{}
  585. for k, v := range validVpsInstances {
  586. healthy := "healthy"
  587. if !healthStatus[k] {
  588. healthy = "unhealthy"
  589. }
  590. mapToTenantDisplay := func(vpsInstances *map[string]map[int]bool, vpsInstanceId string) []TenantDisplay {
  591. toReturn := []TenantDisplay{}
  592. tenantIds, hasTenantIds := (*vpsInstances)[vpsInstanceId]
  593. if hasTenantIds {
  594. for id := range tenantIds {
  595. toReturn = append(toReturn, TenantDisplay{
  596. Name: strconv.Itoa(id),
  597. Color: template.CSS(getTenantColor(id)),
  598. })
  599. }
  600. }
  601. return toReturn
  602. }
  603. display[k] = &VPSInstanceForDisplay{
  604. VPSInstance: *v,
  605. CurrentTenants: mapToTenantDisplay(&allocations, k),
  606. PinnedTenants: mapToTenantDisplay(&pinned, k),
  607. Healthy: healthy,
  608. }
  609. // TODO handle BytesMonthly when node is created in the middle of the month / near end of month
  610. projectedUsage := getInstanceProjectedUsage(v, &allocations, &tenants, desiredInstancesPerTenant, amountOfMonthElapsed)
  611. allowanceBytes := int64(amountOfMonthElapsed * float64(v.BytesMonthly))
  612. usage := (float64(projectedUsage) / float64(v.BytesMonthly)) * float64(0.5)
  613. display[k].ThermometerReal.Color1 = template.CSS("cyan")
  614. if allowanceBytes > v.Bytes {
  615. display[k].ThermometerReal.Color2 = template.CSS("#ddd")
  616. display[k].ThermometerReal.Height1 = int((float64(v.Bytes) / float64(v.BytesMonthly)) * float64(100))
  617. display[k].ThermometerReal.Height2 = int((float64(allowanceBytes-v.Bytes) / float64(v.BytesMonthly)) * float64(100))
  618. } else {
  619. display[k].ThermometerReal.Color2 = template.CSS("orange")
  620. display[k].ThermometerReal.Height1 = int((float64(allowanceBytes) / float64(v.BytesMonthly)) * float64(100))
  621. display[k].ThermometerReal.Height2 = int((float64(allowanceBytes-v.Bytes) / float64(v.BytesMonthly)) * float64(100))
  622. }
  623. display[k].ThermometerProjected.Color1 = template.CSS("green")
  624. if usage < 0.5 {
  625. display[k].ThermometerProjected.Color2 = template.CSS("#ddd")
  626. display[k].ThermometerProjected.Height1 = int(usage * float64(100))
  627. display[k].ThermometerProjected.Height2 = int((float64(0.5) - usage) * float64(100))
  628. } else {
  629. display[k].ThermometerProjected.Color2 = template.CSS("orange")
  630. display[k].ThermometerProjected.Height1 = 50
  631. display[k].ThermometerProjected.Height2 = int((usage - float64(0.5)) * float64(100))
  632. }
  633. }
  634. data := struct {
  635. ValidVPSInstances map[string]*VPSInstanceForDisplay
  636. DBOnlyVPSInstances map[string]*VPSInstance
  637. CloudOnlyVPSInstances map[string]*VPSInstance
  638. HashOfSessionId string
  639. }{
  640. ValidVPSInstances: display,
  641. DBOnlyVPSInstances: dbOnlyInstances,
  642. CloudOnlyVPSInstances: cloudOnlyInstances,
  643. HashOfSessionId: hashOfSessionId,
  644. }
  645. app.buildPageFromTemplate(responseWriter, session, "admin.html", data)
  646. })
  647. app.reloadTemplates()
  648. staticFilesDir := filepath.Join(workingDirectory, "frontend", "static")
  649. log.Printf("serving static files from %s", staticFilesDir)
  650. app.Router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticFilesDir))))
  651. return app
  652. }
  653. func (app *FrontendApp) ListenAndServe() error {
  654. if app.TLSKey != "" && app.TLSCertificate != "" {
  655. return http.ListenAndServeTLS(fmt.Sprintf(":%d", app.Port), app.TLSCertificate, app.TLSKey, app.Router)
  656. } else {
  657. return http.ListenAndServe(fmt.Sprintf(":%d", app.Port), app.Router)
  658. }
  659. }
  660. func (app *FrontendApp) setCookie(responseWriter http.ResponseWriter, name, value string, lifetimeSeconds int, sameSite http.SameSite) {
  661. toSet := &http.Cookie{
  662. Name: name,
  663. Domain: app.Domain,
  664. HttpOnly: true,
  665. Secure: true,
  666. SameSite: sameSite,
  667. Path: "/",
  668. Value: value,
  669. MaxAge: lifetimeSeconds,
  670. }
  671. http.SetCookie(responseWriter, toSet)
  672. }
  673. func (app *FrontendApp) deleteCookie(responseWriter http.ResponseWriter, name string) {
  674. http.SetCookie(responseWriter, &http.Cookie{
  675. Name: name,
  676. Domain: app.Domain,
  677. HttpOnly: true,
  678. Secure: true,
  679. SameSite: http.SameSiteLaxMode,
  680. Path: "/",
  681. Value: "",
  682. MaxAge: -1,
  683. })
  684. }
  685. func (app *FrontendApp) getSession(request *http.Request, domain string) (Session, error) {
  686. toReturn := Session{
  687. Flash: &(map[string]string{}),
  688. }
  689. for _, cookie := range request.Cookies() {
  690. //log.Printf("getSession %t: %s: %s\n", toReturn.SessionId == "", cookie.Name, cookie.Value)
  691. if cookie.Name == "sessionId" || (cookie.Name == "sessionIdLax" && toReturn.SessionId == "") {
  692. app.SessionCacheMutex.Lock()
  693. session, hasSession := app.SessionCache[cookie.Value]
  694. app.SessionCacheMutex.Unlock()
  695. if hasSession {
  696. if time.Now().Before(session.Expires) && (cookie.Name != "sessionIdLax" || session.LaxCookie) {
  697. toReturn.SessionId = cookie.Value
  698. toReturn.TenantId = session.TenantId
  699. toReturn.Email = session.Email
  700. toReturn.EmailVerified = session.EmailVerified
  701. toReturn.LaxCookie = session.LaxCookie
  702. toReturn.Expires = session.Expires
  703. continue
  704. }
  705. }
  706. session, err := app.Model.GetSession(cookie.Value, cookie.Name == "sessionIdLax")
  707. if err != nil {
  708. log.Printf("can't getSession because can't query session from database: %+v", err)
  709. return toReturn, err
  710. }
  711. if session != nil {
  712. app.SessionCacheMutex.Lock()
  713. existingSession, hasExisting := app.SessionIdByTenantId[session.TenantId]
  714. if hasExisting {
  715. delete(app.SessionCache, existingSession)
  716. }
  717. app.SessionIdByTenantId[session.TenantId] = cookie.Value
  718. app.SessionCache[cookie.Value] = session
  719. app.SessionCacheMutex.Unlock()
  720. toReturn.SessionId = cookie.Value
  721. toReturn.TenantId = session.TenantId
  722. toReturn.Email = session.Email
  723. toReturn.EmailVerified = session.EmailVerified
  724. toReturn.LaxCookie = session.LaxCookie
  725. toReturn.Expires = session.Expires
  726. }
  727. //log.Printf("toReturn.SessionId %s\n", toReturn.SessionId)
  728. } else if cookie.Name == "flash" && cookie.Value != "" {
  729. bytes, err := base64.RawURLEncoding.DecodeString(cookie.Value)
  730. if err != nil {
  731. log.Printf("can't getSession because can't base64 decode flash cookie: %+v", err)
  732. return toReturn, err
  733. }
  734. flash := map[string]string{}
  735. err = json.Unmarshal(bytes, &flash)
  736. if err != nil {
  737. log.Printf("can't getSession because can't json parse the decoded flash cookie: %+v", err)
  738. return toReturn, err
  739. }
  740. toReturn.Flash = &flash
  741. }
  742. }
  743. return toReturn, nil
  744. }
  745. func (app *FrontendApp) setSession(responseWriter http.ResponseWriter, session *Session) error {
  746. sessionIdBuffer := make([]byte, 32)
  747. rand.Read(sessionIdBuffer)
  748. sessionId := base64.RawURLEncoding.EncodeToString(sessionIdBuffer)
  749. err := app.Model.SetSession(sessionId, session)
  750. if err != nil {
  751. return err
  752. }
  753. bytes, _ := json.MarshalIndent(session, "", " ")
  754. log.Printf("setSession(): %s %s\n", sessionId, string(bytes))
  755. app.SessionCacheMutex.Lock()
  756. existingSession, hasExisting := app.SessionIdByTenantId[session.TenantId]
  757. if hasExisting {
  758. delete(app.SessionCache, existingSession)
  759. }
  760. app.SessionIdByTenantId[session.TenantId] = sessionId
  761. app.SessionCache[sessionId] = session
  762. app.SessionCacheMutex.Unlock()
  763. exipreInSeconds := int(session.Expires.Sub(time.Now()).Seconds())
  764. if session.LaxCookie {
  765. app.setCookie(responseWriter, "sessionIdLax", sessionId, exipreInSeconds, http.SameSiteLaxMode)
  766. } else {
  767. app.setCookie(responseWriter, "sessionId", sessionId, exipreInSeconds, http.SameSiteStrictMode)
  768. }
  769. return nil
  770. }
  771. func (app *FrontendApp) unhandledError(responseWriter http.ResponseWriter, err error) {
  772. log.Printf("500 internal server error: %+v\n", err)
  773. responseWriter.Header().Add("Content-Type", "text/plain")
  774. responseWriter.WriteHeader(http.StatusInternalServerError)
  775. responseWriter.Write([]byte("500 internal server error"))
  776. }
  777. func (app *FrontendApp) handleWithSpecificUser(path string, userId int, handler func(http.ResponseWriter, *http.Request, Session)) {
  778. app.handleWithSessionImpl(path, true, userId, handler)
  779. }
  780. func (app *FrontendApp) handleWithSession(path string, handler func(http.ResponseWriter, *http.Request, Session)) {
  781. app.handleWithSessionImpl(path, true, 0, handler)
  782. }
  783. func (app *FrontendApp) handleWithSessionNotRequired(path string, handler func(http.ResponseWriter, *http.Request, Session)) {
  784. app.handleWithSessionImpl(path, false, 0, handler)
  785. }
  786. func (app *FrontendApp) handleWithSessionImpl(path string, required bool, requireUserId int, handler func(http.ResponseWriter, *http.Request, Session)) {
  787. app.Router.HandleFunc(path, func(responseWriter http.ResponseWriter, request *http.Request) {
  788. session, err := app.getSession(request, app.Domain)
  789. // bytes, _ := json.MarshalIndent(session, "", " ")
  790. // log.Printf("handleWithSession(): %s\n", string(bytes))
  791. if err != nil {
  792. app.unhandledError(responseWriter, err)
  793. } else {
  794. if (required && session.TenantId == 0) || (requireUserId != 0 && requireUserId != session.TenantId) {
  795. // anti-XSS: only set returnTo if it matches a basic url pattern
  796. if app.basicURLPathRegex.MatchString(request.URL.Path) {
  797. msg := fmt.Sprintf("Please log in in order to access %s%s", app.Domain, request.URL.Path)
  798. app.setFlash(responseWriter, session, "info", msg)
  799. app.setFlash(responseWriter, session, "returnTo", request.URL.Path)
  800. }
  801. http.Redirect(responseWriter, request, "/login", http.StatusFound)
  802. return
  803. }
  804. handler(responseWriter, request, session)
  805. }
  806. })
  807. }
  808. func (app *FrontendApp) buildPage(responseWriter http.ResponseWriter, session Session, highlight, page template.HTML) {
  809. var buffer bytes.Buffer
  810. templateName := "page.html"
  811. pageTemplate, hasPageTemplate := app.HTMLTemplates[templateName]
  812. if !hasPageTemplate {
  813. panic(fmt.Errorf("template '%s' not found!", templateName))
  814. }
  815. err := pageTemplate.Execute(
  816. &buffer,
  817. struct {
  818. Session Session
  819. Highlight template.HTML
  820. Page template.HTML
  821. }{session, highlight, page},
  822. )
  823. app.deleteCookie(responseWriter, "flash")
  824. if err != nil {
  825. app.unhandledError(responseWriter, err)
  826. } else {
  827. io.Copy(responseWriter, &buffer)
  828. }
  829. }
  830. func (app *FrontendApp) renderTemplateToHTML(templateName string, data interface{}) (template.HTML, error) {
  831. var buffer bytes.Buffer
  832. desiredTemplate, hasTemplate := app.HTMLTemplates[templateName]
  833. if !hasTemplate {
  834. return "", fmt.Errorf("template '%s' not found!", templateName)
  835. }
  836. err := desiredTemplate.Execute(&buffer, data)
  837. if err != nil {
  838. return "", err
  839. }
  840. return template.HTML(buffer.String()), nil
  841. }
  842. func (app *FrontendApp) buildPageFromTemplate(responseWriter http.ResponseWriter, session Session, templateName string, data interface{}) {
  843. content, err := app.renderTemplateToHTML(templateName, data)
  844. if err != nil {
  845. app.unhandledError(responseWriter, err)
  846. } else {
  847. app.buildPage(responseWriter, session, template.HTML(""), content)
  848. }
  849. }
  850. func (app *FrontendApp) setFlash(responseWriter http.ResponseWriter, session Session, key, value string) {
  851. (*session.Flash)[key] += value
  852. bytes, err := json.Marshal((*session.Flash))
  853. if err != nil {
  854. log.Printf("can't setFlash because can't json marshal the flash map: %+v", err)
  855. return
  856. }
  857. app.setCookie(responseWriter, "flash", base64.RawURLEncoding.EncodeToString(bytes), 60, http.SameSiteStrictMode)
  858. }
  859. func (app *FrontendApp) reloadTemplates() {
  860. loadTemplate := func(filename string) *template.Template {
  861. newTemplateString, err := ioutil.ReadFile(filename)
  862. if err != nil {
  863. panic(err)
  864. }
  865. newTemplate, err := template.New(filename).Parse(string(newTemplateString))
  866. if err != nil {
  867. panic(err)
  868. }
  869. return newTemplate
  870. }
  871. frontendDirectory := filepath.Join(app.WorkingDirectory, "frontend")
  872. //frontendVersion = hashTemplateAndStaticFiles(frontendDirectory)[:6]
  873. fileInfos, err := ioutil.ReadDir(frontendDirectory)
  874. if err != nil {
  875. panic(err)
  876. }
  877. for _, fileInfo := range fileInfos {
  878. if !fileInfo.IsDir() && strings.Contains(fileInfo.Name(), ".gotemplate") {
  879. app.HTMLTemplates[strings.Replace(fileInfo.Name(), ".gotemplate", "", 1)] = loadTemplate(filepath.Join(frontendDirectory, fileInfo.Name()))
  880. }
  881. }
  882. }
  883. // func hashTemplateAndStaticFiles(workingDirectory string) string {
  884. // filenameMatch := regexp.MustCompile("(\\.gotemplate)|(\\.html)|(\\.css)|(\\.js)$")
  885. // toHash := map[string]bool{}
  886. // var getFileNamesRecurse func(workingDirectory string, path string, depth int)
  887. // getFileNamesRecurse = func(workingDirectory string, path string, depth int) {
  888. // if depth > 10 {
  889. // panic(errors.New("too much recursion inside hashTemplateAndStaticFiles()"))
  890. // }
  891. // fileInfos, err := ioutil.ReadDir(filepath.Join(workingDirectory, path))
  892. // if err != nil {
  893. // panic(err)
  894. // }
  895. // for _, fileInfo := range fileInfos {
  896. // if fileInfo.IsDir() {
  897. // getFileNamesRecurse(workingDirectory, filepath.Join(path, fileInfo.Name()), depth+1)
  898. // } else if filenameMatch.Match([]byte(fileInfo.Name())) {
  899. // toHash[filepath.Join(path, fileInfo.Name())] = true
  900. // }
  901. // }
  902. // }
  903. // toHashSlice := sort.StringSlice(make([]string, len(toHash)))
  904. // i := 0
  905. // for filename := range toHash {
  906. // toHashSlice[i] = filename
  907. // i++
  908. // }
  909. // toHashSlice.Sort()
  910. // hash := sha256.New()
  911. // for _, filename := range toHashSlice {
  912. // fileContents, err := ioutil.ReadFile(filepath.Join(workingDirectory, filename))
  913. // if err != nil {
  914. // panic(err)
  915. // }
  916. // hash.Write([]byte(fileContents))
  917. // }
  918. // return fmt.Sprintf("%x", hash.Sum(nil))
  919. // }
  920. var tenantColors = map[int]string{}
  921. func getTenantColor(tenantId int) string {
  922. if _, has := tenantColors[tenantId]; has {
  923. return tenantColors[tenantId]
  924. }
  925. var randomInt int64
  926. colorHashArray := md5.Sum([]byte(strconv.Itoa(tenantId)))
  927. colorHash := bytes.NewReader(colorHashArray[0:16])
  928. err := binary.Read(colorHash, binary.LittleEndian, &randomInt)
  929. if err != nil {
  930. panic(err)
  931. }
  932. randomSource := mathRand.NewSource(randomInt)
  933. toReturn := hsvColor(
  934. float64(randomSource.Int63()%360),
  935. float64(0.68)+(float64(randomSource.Int63()%80)/float64(255)),
  936. float64(0.68)+(float64(randomSource.Int63()%80)/float64(255)),
  937. )
  938. tenantColors[tenantId] = toReturn
  939. return toReturn
  940. }
  941. func hsvColor(H, S, V float64) string {
  942. Hp := H / 60.0
  943. C := V * S
  944. X := C * (1.0 - math.Abs(math.Mod(Hp, 2.0)-1.0))
  945. m := V - C
  946. r, g, b := 0.0, 0.0, 0.0
  947. switch {
  948. case 0.0 <= Hp && Hp < 1.0:
  949. r = C
  950. g = X
  951. case 1.0 <= Hp && Hp < 2.0:
  952. r = X
  953. g = C
  954. case 2.0 <= Hp && Hp < 3.0:
  955. g = C
  956. b = X
  957. case 3.0 <= Hp && Hp < 4.0:
  958. g = X
  959. b = C
  960. case 4.0 <= Hp && Hp < 5.0:
  961. r = X
  962. b = C
  963. case 5.0 <= Hp && Hp < 6.0:
  964. r = C
  965. b = X
  966. }
  967. return fmt.Sprintf("rgb(%d, %d, %d)", int((m+r)*float64(255)), int((m+g)*float64(255)), int((m+b)*float64(255)))
  968. }
  969. func ByteCountSI(b int64) string {
  970. const unit = 1000
  971. if b < unit {
  972. return fmt.Sprintf("%d B", b)
  973. }
  974. div, exp := int64(unit), 0
  975. for n := b / unit; n >= unit; n /= unit {
  976. div *= unit
  977. exp++
  978. }
  979. return fmt.Sprintf("%.1f %cB",
  980. float64(b)/float64(div), "kMGTPE"[exp])
  981. }