gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit4a2d71254a2d7125initial commitmre4a2d7125/project.hl

8.0 KB

  1. // project.hl — gitoria.worldapi.org: THE APP. Routes and WHO HEARS WHAT. Hybriel on hl:web.
  2. //
  3. // HOW A REPO GETS ITS ADDRESS (ticket #6, gitoria#16): nginx sends `gitoria.worldapi.org` AND every
  4. // `<slug>.gitoria.worldapi.org` to this one app. Every page component that declares `host = null` gets the
  5. // request's host (without port, hybriel#74) on the SERVER, on the first load and on every hl:web navigation:
  6. // the main address shows the repo list + "create" (components/index.hl → list.hl), `<slug>.` shows that repo —
  7. // each repo view is its own page component on its own route (readme, code, branch, commit, pulls, releases,
  8. // tickets), rendered on the server WITH its content: git is read with hl:proc run(), which waits (hybriel#80).
  9. // The session cookie carries `Domain=.<host>` (hl:web `sessionDomain`, hybriel#44) so one
  10. // login holds on every address; the login button always returns through the main address
  11. // (the only origin registered in ident) and then on to the repo the login started from.
  12. import WebFramework from 'hl:web'
  13. import { env } from 'hl:proc'
  14. import { Response } from 'hl:http1'
  15. import { randomBytes } from 'hl:crypto'
  16. import Styles from './styles.hl'
  17. import { reply, fail, wantsMarkdown, markdownReply } from './api.hl'
  18. import { repoRows, repoRow, repoDocument, listDocument, slugError } from './repos.hl'
  19. import { exchangeCode, ensureUser, hostPort, hostOnly, scheme, domainFor } from './users.hl'
  20. import { gitTransport } from './transport.hl'
  21. import { gitKeys, gitAccess } from './sshgate.hl'
  22. import Index from './components/index.hl'
  23. import Code from './components/code.hl'
  24. import Branch from './components/branch.hl'
  25. import Commit from './components/commit.hl'
  26. import Pulls from './components/pulls.hl'
  27. import Releases from './components/releases.hl'
  28. import Tickets from './components/tickets.hl'
  29. import LoginFailed from './components/loginfailed.hl'
  30. static siteName = "gitoria"
  31. appTitle = siteName
  32. styles = Styles
  33. // ---- the API: reads are public (creating a repo is a web action for now) ------------------------
  34. apiRepos = (route, req) => {
  35. if (req.method != 'GET') { return fail(405, 'GET only') }
  36. let rows = repoRows()
  37. if (wantsMarkdown(req)) { return markdownReply(listDocument(rows)) }
  38. return { repos = rows }
  39. }
  40. apiRepo = (route, req) => {
  41. if (req.method != 'GET') { return fail(405, 'GET only') }
  42. let row = repoRow(route.params.slug)
  43. if (row == null) { return fail(404, 'no such repository') }
  44. if (wantsMarkdown(req)) { return markdownReply(repoDocument(row)) }
  45. return row
  46. }
  47. // ---- THE LOGIN BUTTON'S RETURN (ident README "How apps use ident") ----------------------------
  48. // BACK TO THE PAGE: /login.js puts `?next=` into the button's return URL at the click. Only a same-origin PATH
  49. // goes (one `/`, URL-safe characters, ≤ 500) — or a full URL of THIS system: <scheme>://<label>.<host>
  50. // with a valid, non-reserved repo label and such a path. Anything else → `/`.
  51. nextChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~/?&=%+,;@!$()*:'
  52. safePath = (want) => {
  53. if (want == null || hlTypeName(want) != 'String' || want == '' || want.length > 500) { return '/' }
  54. if (want.slice(0, 1) != '/' || want.slice(0, 2) == '//' || want.slice(0, 7) == '/login/') { return '/' }
  55. let i = 0
  56. while (i < want.length) {
  57. if (!nextChars.includes(want[i])) { return '/' }
  58. i = i + 1
  59. }
  60. return want
  61. }
  62. safeNext = (want) => {
  63. if (want == null || hlTypeName(want) != 'String' || want.length > 500) { return '/' }
  64. let prefix = scheme + '://'
  65. if (!want.startsWith(prefix)) { return safePath(want) }
  66. let rest = want.slice(prefix.length)
  67. let slash = rest.indexOf('/')
  68. let hp = slash < 0 ? rest : rest.slice(0, slash)
  69. let path = slash < 0 ? '/' : rest.slice(slash)
  70. let dot = hp.indexOf('.')
  71. if (dot < 1 || hp.slice(dot + 1) != hostPort || slugError(hp.slice(0, dot)) != null) { return '/' }
  72. let p = safePath(path)
  73. if (p == '/' && path != '/') { return '/' }
  74. return prefix + hp + p
  75. }
  76. // A FAILED LOGIN is a page (components/loginfailed.hl): the reason is parked in the session, then → /login/failed
  77. failed = (req, why) => {
  78. let s = req.session
  79. let fresh = s == null
  80. if (fresh) { s = server.sessions.mint() }
  81. s.data.loginError = why
  82. server.sessions.save(s)
  83. let res = new Response('login failed: ' + why, { status = 302 headers = { 'Location' = '/login/failed' 'Cache-Control' = 'no-store' 'Content-Type' = 'text/plain; charset=utf-8' } })
  84. if (fresh) { res.headers['Set-Cookie'] = server.sessions.cookieHeader(s.id) }
  85. return res
  86. }
  87. // the function route gets the cookie's session as req.session (hybriel #11); none yet → minted here
  88. loginCallback = (route, req) => {
  89. if (req.method != 'GET') { return failed(req, 'GET only') }
  90. let q = req.query != null ? req.query : {}
  91. let code = q.ident_code
  92. if (code == null || code == '') { return failed(req, 'ident sent no login code') }
  93. let x = exchangeCode(code)
  94. if (x.error != null) { return failed(req, x.error) }
  95. let u = ensureUser(x.identity)
  96. if (u == null) { return failed(req, 'could not store the user') }
  97. let s = req.session
  98. let fresh = s == null
  99. if (fresh) { s = server.sessions.mint() }
  100. s.user = { id = u.id }
  101. s.data.tag = randomBytes(16)
  102. s.data.loginError = null
  103. server.sessions.save(s)
  104. let res = new Response('logged in', { status = 302 headers = { 'Location' = safeNext(q.next) 'Cache-Control' = 'no-store' 'Content-Type' = 'text/plain; charset=utf-8' } })
  105. if (fresh) { res.headers['Set-Cookie'] = server.sessions.cookieHeader(s.id) }
  106. return res
  107. }
  108. // EVERY REPO VIEW IS ITS OWN PAGE (gitoria#16); `/` is the list on the main address, the Readme on a repo address.
  109. routes = [
  110. { pattern = "/favicon.ico" direct = "" }
  111. { pattern = "/login/callback" function = loginCallback }
  112. { pattern = "/login/failed" component = LoginFailed }
  113. { pattern = "/login.js" file = "./login.js" headers = { 'Cache-Control' = 'no-cache' } }
  114. { pattern = "/api/repos" function = apiRepos }
  115. { pattern = "/api/repos/:slug" function = apiRepo }
  116. { pattern = "/" component = Index }
  117. { pattern = "/code" component = Code }
  118. { pattern = "/code/*path" component = Code }
  119. { pattern = "/branch/*path" component = Branch }
  120. { pattern = "/commit/*path" component = Commit }
  121. { pattern = "/pulls" component = Pulls }
  122. { pattern = "/releases" component = Releases }
  123. { pattern = "/tickets" component = Tickets }
  124. { pattern = "/__git/keys" function = gitKeys }
  125. { pattern = "/__git/access" function = gitAccess }
  126. { pattern = "/:repo/info/refs" function = gitTransport }
  127. { pattern = "/:repo/git-upload-pack" function = gitTransport }
  128. { pattern = "/:repo/git-receive-pack" function = gitTransport }
  129. ]
  130. // WHO GETS THE PUSH: a new repo reaches every open page (the list follows live)
  131. // `gitoriaSignedIn` / `gitoriaSignedOut` go to the tabs of ONE session: the one whose login carries
  132. // that random tag (session.data.tag, set at login) — every open page of it switches without a reload.
  133. tagOf = (session) => { return session != null && session.data != null ? session.data.tag : null }
  134. audience = {
  135. repoCreated = (row, session) => { return true }
  136. ticketOpened = (slug, row, session) => { return true }
  137. gitoriaSignedIn = (tag, info, session) => { return tag != null && tagOf(session) == tag }
  138. gitoriaSignedOut = (tag, session) => { return tag != null && tagOf(session) == tag }
  139. }
  140. sessionDir = env('GITORIA_SESSIONS') != null ? env('GITORIA_SESSIONS') : null
  141. port = env('GITORIA_PORT') != null ? toNumber(env('GITORIA_PORT')) : 8360
  142. // HL_HOST = the interface hl:web binds (hybriel #24, fixed upstream): 127.0.0.1 on Byrodin behind nginx;
  143. // unset = 0.0.0.0 (dev on Loreana). GITORIA_WATCH=0 = no dev watcher. The session cookie is `gitoriasid` (hybriel #10).
  144. watching = env('GITORIA_WATCH') != '0'
  145. sessionCookie = 'gitoriasid'
  146. // the cookie's Domain (hybriel#44): every <slug>.<host> shares the login (users.hl domainFor; '' = host-only → null)
  147. sessionDomain = domainFor(hostOnly) != '' ? domainFor(hostOnly) : null
  148. server = new WebFramework(routes = routes, styles = styles, minify = true, port = port, watchMode = watching, sessionCookie = sessionCookie, sessionDomain = sessionDomain)
  149. on Error(e) { console.log('error absorbed: ' + e.message) }

Branches

Latest commits

  • 4a2d7125initial commitmre