gitlyx / repo / git_routes.v
200 lines · 153 sloc · 5.86 KB · 13ca859655a005b2a87a4935764694302b4f2fda
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 if auth_header_parts.len != 2 {
123 return false
124 }
125 is_basic_auth_type := auth_header_parts[0] == 'Basic'
126 return is_basic_auth_type
127}
128
129fn (ctx &Context) extract_user_credentials() ?(string, string) {
130 auth_header := ctx.get_header(.authorization) or { return none }
131 auth_header_parts := auth_header.fields()
132
133 if auth_header_parts.len < 2 {
134 return none
135 }
136
137 return decode_basic_auth(auth_header_parts[1])
138}
139
140fn (mut app App) check_user_credentials(ctx &Context) bool {
141 username, password := ctx.extract_user_credentials() or { return false }
142 user := app.get_user_by_username(username) or { return false }
143
144 return compare_password_with_hash(password, user.salt, user.password)
145}
146
147fn (mut app Context) set_no_cache_headers() {
148 app.set_header(.expires, 'Fri, 01 Jan 1980 00:00:00 GMT')
149 app.set_header(.pragma, 'no-cache')
150 app.set_header(.cache_control, 'no-cache, max-age=0, must-revalidate')
151}
152
153fn (mut app Context) set_authenticate_headers() {
154 app.set_header(.www_authenticate, 'Basic realm="."')
155}
156
157fn (mut app Context) set_git_content_type_headers(service GitService) {
158 if service == .upload {
159 app.set_content_type('application/x-git-upload-pack-result')
160 } else if service == .receive {
161 app.set_content_type('application/x-git-receive-pack-result')
162 }
163}
164
165fn (mut app Context) send_internal_error(custom_message string) {
166 message := if custom_message == '' { 'Internal Server error' } else { custom_message }
167
168 app.send_custom_error(500, message)
169}
170
171fn (mut app Context) send_unauthorized() {
172 app.send_custom_error(401, 'Unauthorized')
173}
174
175fn (mut app Context) send_not_found() {
176 app.send_custom_error(404, 'Not Found')
177}
178
179fn (mut app Context) send_custom_error(code int, text string) {
180 // app.set_status(code, text)
181 app.res.set_status(unsafe { http.Status(code) })
182 app.send_response_to_client(veb.mime_types['.txt'], '')
183}
184
185fn (mut app Context) parse_body() string {
186 body := app.req.data
187
188 if h := app.get_header(.content_encoding) {
189 if h == 'gzip' {
190 decompressed := deflate.decompress(body.bytes()[10..]) or {
191 println(err)
192 return body
193 }
194
195 return decompressed.bytestr()
196 }
197 }
198
199 return body
200}
201