PageRenderTime 22ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/components/ws-proxy/pkg/proxy/workspacerouter.go

https://gitlab.com/geropl/gitpod
Go | 196 lines | 137 code | 25 blank | 34 comment | 37 complexity | 2c23f5c97ea15635dd76a08bfb8b383e MD5 | raw file
  1. // Copyright (c) 2020 Gitpod GmbH. All rights reserved.
  2. // Licensed under the GNU Affero General Public License (AGPL).
  3. // See License-AGPL.txt in the project root for license information.
  4. package proxy
  5. import (
  6. "net/http"
  7. "regexp"
  8. "strings"
  9. "github.com/gorilla/mux"
  10. "github.com/gitpod-io/gitpod/common-go/log"
  11. )
  12. const (
  13. // Used as key for storing the workspace port in the requests mux.Vars() map.
  14. workspacePortIdentifier = "workspacePort"
  15. // Used as key for storing the workspace ID in the requests mux.Vars() map.
  16. workspaceIDIdentifier = "workspaceID"
  17. // Used as key for storing the origin to fetch foreign content.
  18. foreignOriginIdentifier = "foreignOrigin"
  19. // Used as key for storing the path to fetch foreign content.
  20. foreignPathIdentifier = "foreignPath"
  21. // The header that is used to communicate the "Host" from proxy -> ws-proxy in scenarios where ws-proxy is _not_ directly exposed.
  22. forwardedHostnameHeader = "x-wsproxy-host"
  23. // This pattern matches v4 UUIDs as well as the new generated workspace ids (e.g. pink-panda-ns35kd21).
  24. workspaceIDRegex = "(?P<" + workspaceIDIdentifier + ">[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-z]{2,16}-[0-9a-z]{2,16}-[0-9a-z]{8,11})"
  25. workspacePortRegex = "(?P<" + workspacePortIdentifier + ">[0-9]+)-"
  26. )
  27. // WorkspaceRouter is a function that configures subrouters (one for theia, one for the exposed ports) on the given router
  28. // which resolve workspace coordinates (ID, port?) from each request. The contract is to store those in the request's mux.Vars
  29. // with the keys workspacePortIdentifier and workspaceIDIdentifier.
  30. type WorkspaceRouter func(r *mux.Router, wsInfoProvider WorkspaceInfoProvider) (ideRouter *mux.Router, portRouter *mux.Router, blobserveRouter *mux.Router)
  31. // HostBasedRouter is a WorkspaceRouter that routes simply based on the "Host" header.
  32. func HostBasedRouter(header, wsHostSuffix string, wsHostSuffixRegex string) WorkspaceRouter {
  33. return func(r *mux.Router, wsInfoProvider WorkspaceInfoProvider) (*mux.Router, *mux.Router, *mux.Router) {
  34. allClusterWsHostSuffixRegex := wsHostSuffixRegex
  35. if allClusterWsHostSuffixRegex == "" {
  36. allClusterWsHostSuffixRegex = wsHostSuffix
  37. }
  38. var (
  39. getHostHeader = func(req *http.Request) string {
  40. host := req.Header.Get(header)
  41. // if we don't get host from special header, fallback to use req.Host
  42. if header == "Host" || host == "" {
  43. parts := strings.Split(req.Host, ":")
  44. return parts[0]
  45. }
  46. return host
  47. }
  48. blobserveRouter = r.MatcherFunc(matchBlobserveHostHeader(wsHostSuffix, getHostHeader)).Subrouter()
  49. portRouter = r.MatcherFunc(matchWorkspaceHostHeader(wsHostSuffix, getHostHeader, true)).Subrouter()
  50. ideRouter = r.MatcherFunc(matchWorkspaceHostHeader(allClusterWsHostSuffixRegex, getHostHeader, false)).Subrouter()
  51. )
  52. r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
  53. hostname := getHostHeader(req)
  54. log.Debugf("no match for path %s, host: %s", req.URL.Path, hostname)
  55. w.WriteHeader(http.StatusNotFound)
  56. })
  57. return ideRouter, portRouter, blobserveRouter
  58. }
  59. }
  60. type hostHeaderProvider func(req *http.Request) string
  61. func matchWorkspaceHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider, matchPort bool) mux.MatcherFunc {
  62. regexPrefix := workspaceIDRegex
  63. if matchPort {
  64. regexPrefix = workspacePortRegex + workspaceIDRegex
  65. }
  66. r := regexp.MustCompile("^" + regexPrefix + wsHostSuffix)
  67. foreignContentHostR := regexp.MustCompile("^(.+)(?:foreign)" + wsHostSuffix)
  68. foreignContentHost2R := regexp.MustCompile("^((?:v--)?[0-9a-v]+)" + wsHostSuffix)
  69. foreignContentPathR := regexp.MustCompile("^/" + regexPrefix + "(/.*)")
  70. return func(req *http.Request, m *mux.RouteMatch) bool {
  71. hostname := headerProvider(req)
  72. if hostname == "" {
  73. return false
  74. }
  75. var workspaceID, workspacePort, foreignOrigin, foreignPath string
  76. matches := foreignContentHostR.FindStringSubmatch(hostname)
  77. if len(matches) == 0 {
  78. matches = foreignContentHost2R.FindStringSubmatch(hostname)
  79. }
  80. if len(matches) == 2 {
  81. foreignOrigin = matches[1]
  82. matches = foreignContentPathR.FindStringSubmatch(req.URL.Path)
  83. if matchPort {
  84. if len(matches) < 4 {
  85. return false
  86. }
  87. // https://extensions-foreign.ws-eu10.gitpod.io/3000-coral-dragon-ilr0r6eq/index.html
  88. // workspaceID: coral-dragon-ilr0r6eq
  89. // workspacePort: 3000
  90. // foreignOrigin: extensions-
  91. // foreignPath: /index.html
  92. workspaceID = matches[2]
  93. workspacePort = matches[1]
  94. foreignPath = matches[3]
  95. } else {
  96. if len(matches) < 3 {
  97. return false
  98. }
  99. // https://extensions-foreign.ws-eu10.gitpod.io/coral-dragon-ilr0r6eq/index.html
  100. // workspaceID: coral-dragon-ilr0r6eq
  101. // workspacePort:
  102. // foreignOrigin: extensions-
  103. // foreignPath: /index.html
  104. workspaceID = matches[1]
  105. foreignPath = matches[2]
  106. }
  107. } else {
  108. matches = r.FindStringSubmatch(hostname)
  109. if matchPort {
  110. if len(matches) < 3 {
  111. return false
  112. }
  113. // https://3000-coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
  114. // workspaceID: coral-dragon-ilr0r6eq
  115. // workspacePort: 3000
  116. // foreignOrigin:
  117. // foreignPath:
  118. workspaceID = matches[2]
  119. workspacePort = matches[1]
  120. } else {
  121. if len(matches) < 2 {
  122. return false
  123. }
  124. // https://coral-dragon-ilr0r6eq.ws-eu10.gitpod.io/index.html
  125. // workspaceID: coral-dragon-ilr0r6eq
  126. // workspacePort:
  127. // foreignOrigin:
  128. // foreignPath:
  129. workspaceID = matches[1]
  130. }
  131. }
  132. if workspaceID == "" {
  133. return false
  134. }
  135. if matchPort && workspacePort == "" {
  136. return false
  137. }
  138. if m.Vars == nil {
  139. m.Vars = make(map[string]string)
  140. }
  141. m.Vars[workspaceIDIdentifier] = workspaceID
  142. if workspacePort != "" {
  143. m.Vars[workspacePortIdentifier] = workspacePort
  144. }
  145. if foreignOrigin != "" {
  146. m.Vars[foreignOriginIdentifier] = foreignOrigin
  147. }
  148. if foreignPath != "" {
  149. m.Vars[foreignPathIdentifier] = foreignPath
  150. }
  151. return true
  152. }
  153. }
  154. func matchBlobserveHostHeader(wsHostSuffix string, headerProvider hostHeaderProvider) mux.MatcherFunc {
  155. r := regexp.MustCompile("^blobserve" + wsHostSuffix)
  156. return func(req *http.Request, m *mux.RouteMatch) bool {
  157. hostname := headerProvider(req)
  158. if hostname == "" {
  159. return false
  160. }
  161. matches := r.FindStringSubmatch(hostname)
  162. return len(matches) >= 1
  163. }
  164. }
  165. func getWorkspaceCoords(req *http.Request) WorkspaceCoords {
  166. vars := mux.Vars(req)
  167. return WorkspaceCoords{
  168. ID: vars[workspaceIDIdentifier],
  169. Port: vars[workspacePortIdentifier],
  170. }
  171. }