ggdgsdbsdbbb / repo / git_routes.v
198 lines · 151 sloc · 5.87 KB · e7b0a33988e6e160c40a10b184ccb38bd1cfa279
Raw
1// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
3module main
4
5import veb
6import git
7import compress.deflate
8import net.http
9
10@['/:username/:repo_name/info/refs']
11fn (mut app App) handle_git_info(username string, git_repo_name string) veb.Result {
12 repo_name := git.remove_git_extension_if_exists(git_repo_name)
13 user := app.get_user_by_username(username) or { return ctx.not_found() }
14 repo := app.find_repo_by_name_and_user_id(repo_name, user.id) or { return ctx.not_found() }
15 service := extract_service_from_url(ctx.req.url)
16
17 if service == .unknown {
18 return ctx.not_found()
19 }
20
21 is_receive_service := service == .receive
22 is_private_repo := !repo.is_public
23
24 if is_receive_service || is_private_repo {
25 app.check_git_http_access(mut ctx, username, repo_name) or { return veb.no_result() }
26 }
27
28 refs := repo.git_advertise(service.str())
29 git_response := build_git_service_response(service, refs)
30
31 ctx.set_content_type('application/x-git-${service}-advertisement')
32 ctx.set_no_cache_headers()
33
34 return ctx.ok(git_response)
35}
36
37@['/:user/:repo_name/git-upload-pack'; post]
38fn (mut app App) handle_git_upload_pack(username string, git_repo_name string) veb.Result {
39 body := ctx.parse_body()
40 repo_name := git.remove_git_extension_if_exists(git_repo_name)
41 user := app.get_user_by_username(username) or { return ctx.not_found() }
42 repo := app.find_repo_by_name_and_user_id(repo_name, user.id) or { return ctx.not_found() }
43 is_private_repo := !repo.is_public
44
45 if is_private_repo {
46 app.check_git_http_access(mut ctx, username, repo_name) or { return veb.no_result() }
47 }
48
49 git_response := repo.git_smart('upload-pack', body)
50
51 ctx.set_git_content_type_headers(.upload)
52
53 return ctx.ok(git_response)
54}
55
56@['/:user/:repo_name/git-receive-pack'; post]
57fn (mut app App) handle_git_receive_pack(username string, git_repo_name string) veb.Result {
58 body := ctx.parse_body()
59 repo_name := git.remove_git_extension_if_exists(git_repo_name)
60 user := app.get_user_by_username(username) or { return ctx.not_found() }
61 repo := app.find_repo_by_name_and_user_id(repo_name, user.id) or { return ctx.not_found() }
62
63 app.check_git_http_access(mut ctx, username, repo_name) or { return veb.no_result() }
64
65 git_response := repo.git_smart('receive-pack', body)
66
67 branch_name := git.parse_branch_name_from_receive_upload(body) or {
68 return ctx.server_error('Receive upload parsing error')
69 }
70
71 app.update_repo_after_push(repo.id, branch_name) or {
72 return ctx.server_error('There was an error while updating the repo')
73 }
74
75 // Language analysis reads every file in the repo and is slow; run it in
76 // a background thread with its own DB connection so the git client is
77 // not blocked.
78 spawn bg_recompute_lang_stats(repo.id, app.config)
79
80 // Trigger CI if .gitly-ci.yml exists in the repo
81 spawn app.trigger_ci_if_configured(repo.id, branch_name)
82
83 ctx.set_git_content_type_headers(.receive)
84
85 return ctx.ok(git_response)
86}
87
88fn (mut app App) check_git_http_access(mut ctx Context, repository_owner string, repository_name string) ?bool {
89 has_valid_auth_header := ctx.check_basic_authorization_header()
90
91 if !has_valid_auth_header {
92 ctx.set_authenticate_headers()
93 ctx.send_unauthorized()
94 return none
95 }
96
97 has_user_valid_credentials := app.check_user_credentials(ctx)
98
99 if has_user_valid_credentials {
100 username, _ := ctx.extract_user_credentials() or {
101 ctx.send_unauthorized()
102 return none
103 }
104
105 has_user_access := repository_owner == username
106
107 if has_user_access {
108 return true
109 } else {
110 ctx.send_not_found()
111 return none
112 }
113 }
114
115 ctx.send_unauthorized()
116 return none
117}
118
119fn (ctx &Context) check_basic_authorization_header() bool {
120 auth_header := ctx.get_header(.authorization) or { return false }
121 auth_header_parts := auth_header.fields()
122 auth_type := auth_header_parts[0]
123 is_basic_auth_type := auth_type == 'Basic'
124 return auth_header_parts.len == 2 || is_basic_auth_type
125}
126
127fn (ctx &Context) extract_user_credentials() ?(string, string) {
128 auth_header := ctx.get_header(.authorization) or { return none }
129 auth_header_parts := auth_header.fields()
130
131 if auth_header_parts.len < 2 {
132 return none
133 }
134
135 return decode_basic_auth(auth_header_parts[1])
136}
137
138fn (mut app App) check_user_credentials(ctx &Context) bool {
139 username, password := ctx.extract_user_credentials() or { return false }
140 user := app.get_user_by_username(username) or { return false }
141
142 return compare_password_with_hash(password, user.salt, user.password)
143}
144
145fn (mut app Context) set_no_cache_headers() {
146 app.set_header(.expires, 'Fri, 01 Jan 1980 00:00:00 GMT')
147 app.set_header(.pragma, 'no-cache')
148 app.set_header(.cache_control, 'no-cache, max-age=0, must-revalidate')
149}
150
151fn (mut app Context) set_authenticate_headers() {
152 app.set_header(.www_authenticate, 'Basic realm="."')
153}
154
155fn (mut app Context) set_git_content_type_headers(service GitService) {
156 if service == .upload {
157 app.set_content_type('application/x-git-upload-pack-result')
158 } else if service == .receive {
159 app.set_content_type('application/x-git-receive-pack-result')
160 }
161}
162
163fn (mut app Context) send_internal_error(custom_message string) {
164 message := if custom_message == '' { 'Internal Server error' } else { custom_message }
165
166 app.send_custom_error(500, message)
167}
168
169fn (mut app Context) send_unauthorized() {
170 app.send_custom_error(401, 'Unauthorized')
171}
172
173fn (mut app Context) send_not_found() {
174 app.send_custom_error(404, 'Not Found')
175}
176
177fn (mut app Context) send_custom_error(code int, text string) {
178 // app.set_status(code, text)
179 app.res.set_status(unsafe { http.Status(code) })
180 app.send_response_to_client(veb.mime_types['.txt'], '')
181}
182
183fn (mut app Context) parse_body() string {
184 body := app.req.data
185
186 if h := app.get_header(.content_encoding) {
187 if h == 'gzip' {
188 decompressed := deflate.decompress(body.bytes()[10..]) or {
189 println(err)
190 return body
191 }
192
193 return decompressed.bytestr()
194 }
195 }
196
197 return body
198}
199