Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-workhorse
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kirill Smelkov
gitlab-workhorse
Commits
81b9a523
Commit
81b9a523
authored
Nov 30, 2015
by
Kirill Smelkov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
.
parent
c99926dc
Pipeline
#120
failed with stage
Changes
1
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
55 additions
and
36 deletions
+55
-36
blob.go
blob.go
+55
-36
No files found.
blob.go
View file @
81b9a523
/*
Handler for raw blob downloads
*/
// Handler for raw blob downloads
//
// Blobs are read via `git cat-file ...` with first querying authentication
// backend about download-access for containing repository. Replies from
// authentication backend are cached for 30 seconds to keep access-to-blobs
// latency to minimum.
package
main
...
...
@@ -30,7 +33,7 @@ type AuthCacheEntry struct {
Tauth
int64
// in seconds XXX do we strictly need this?
// how many times this entry was hit when qu
ier
ing auth cache during
// how many times this entry was hit when qu
ery
ing auth cache during
// the last refresh period.
Nhit
int64
}
...
...
@@ -42,10 +45,11 @@ type AuthCacheEntry struct {
var
authCache
=
make
(
map
[
string
]
*
AuthCacheEntry
)
// Time period for refreshing / removing unused entires in authCache
const
authCacheRefresh
=
5
*
time
.
Second
// XXX -> 30 or 60
const
authCacheRefresh
=
5
*
time
.
Second
// XXX -> 30
// Goroutine to refresh auth cache entry periodically while it is used.
// if the entry is detected to be not used - remove it from cache and stop rereshing.
// if the entry is detected to be not used - remove it from cache and stop re
f
reshing.
func
authRefreshEntry
(
u
*
upstream
,
project
string
)
{
// XXX auth := authCache[project]
// and then use auth without authCache lookup ?
...
...
@@ -54,7 +58,7 @@ func authRefreshEntry(u *upstream, project string) {
//log.Printf("AUTH refresh sleep ...")
time
.
Sleep
(
authCacheRefresh
)
// XXX lock
?
// XXX lock
auth
,
ok
:=
authCache
[
project
]
if
!
ok
{
// someone removed the entry from cache - no
log
.
Printf
(
"AUTH refresh - %v entry removed"
,
project
)
...
...
@@ -64,15 +68,22 @@ func authRefreshEntry(u *upstream, project string) {
log
.
Printf
(
"AUTH refresh - %v #hit: %v"
,
project
,
auth
.
Nhit
)
if
auth
.
Nhit
==
0
{
// not used - we can remove and stop refreshing
log
.
Printf
(
"AUTH - removing %v"
,
project
)
// XXX lock
?
// XXX lock
delete
(
authCache
,
project
)
break
}
log
.
Printf
(
"AUTH - refreshing %v"
,
project
)
authReply
:=
askAuthBackend
(
u
,
project
)
// XXX what if it stucks?
authReply
,
err
:=
askAuthBackend
(
u
,
project
)
if
err
!=
nil
{
// an error -> delete entry from cache and be done with
// refreshing XXX lock, unify with ^^^
delete
(
authCache
,
project
)
break
}
// XXX lock
?
// XXX lock
auth
.
AuthReply
=
authReply
auth
.
Tauth
=
time
.
Now
()
.
Unix
()
auth
.
Nhit
=
0
...
...
@@ -82,7 +93,7 @@ func authRefreshEntry(u *upstream, project string) {
// Ask auth backend about whether download is ok for a project
func
askAuthBackend
(
u
*
upstream
,
project
string
)
AuthReply
{
func
askAuthBackend
(
u
*
upstream
,
project
string
)
(
AuthReply
,
error
)
{
authReply
:=
AuthReply
{
w
:
httptest
.
NewRecorder
(),
}
...
...
@@ -93,7 +104,7 @@ func askAuthBackend(u *upstream, project string) AuthReply {
reqDownloadAccess
,
err
:=
http
.
NewRequest
(
"GET"
,
project
+
".git/info/refs?service=git-upload-pack"
,
nil
)
if
err
!=
nil
{
fail500
(
authReply
.
w
,
"GET git-upload-pack"
,
err
)
return
authReply
return
authReply
,
err
}
// prepare everything and go through preAuthorizeHandler that will send
...
...
@@ -103,6 +114,7 @@ func askAuthBackend(u *upstream, project string) AuthReply {
u
:
u
,
}
// XXX what if it gets stuck?
preAuthorizeHandler
(
func
(
w
http
.
ResponseWriter
,
r
*
gitRequest
)
{
// if we ever get to this point - auth handler approved
...
...
@@ -113,13 +125,14 @@ func askAuthBackend(u *upstream, project string) AuthReply {
// propagate authorizationResponse back and we are done
authReply
.
authorizationResponse
=
r
.
authorizationResponse
return
authReply
return
authReply
,
nil
}
// Verify that download access is authorized by auth backend
func
verifyDownloadAccess
(
w
http
.
ResponseWriter
,
u
*
upstream
,
project
string
)
AuthReply
{
// XXX do we need mutex to lock authCache ?
// Verify that download access is ok or not.
// first we try to see authCache; if information is not there -> ask auth backend
func
verifyDownloadAccess
(
w
http
.
ResponseWriter
,
u
*
upstream
,
project
string
)
(
AuthReply
,
error
)
{
// XXX lock authCache
auth
,
ok
:=
authCache
[
project
]
if
ok
{
auth
.
Nhit
++
...
...
@@ -127,44 +140,49 @@ func verifyDownloadAccess(w http.ResponseWriter, u *upstream, project string) Au
project
,
time
.
Since
(
time
.
Unix
(
auth
.
Tauth
,
0
)),
auth
.
Nhit
)
return
auth
.
AuthReply
// XXX make pointer?
return
auth
.
AuthReply
,
nil
// XXX make pointer?
}
authReply
:=
askAuthBackend
(
u
,
project
)
authReply
,
err
:=
askAuthBackend
(
u
,
project
)
if
err
!=
nil
{
return
authReply
,
err
}
// XXX do we need to lock authCache ?
// store in cache and start cache entry refresher
authCache
[
project
]
=
&
AuthCacheEntry
{
authReply
,
time
.
Now
()
.
Unix
(),
0
}
go
authRefreshEntry
(
u
,
project
)
return
authReply
return
authReply
,
nil
}
// HTTP handler for
.../raw/<ref>/path
// HTTP handler for
`.../raw/<ref>/path`
var
projectRe
=
regexp
.
MustCompile
(
`^/[\w\.-]+/[\w\.-]+/`
)
func
handleGetBlobRaw
(
w
http
.
ResponseWriter
,
r
*
gitRequest
)
{
// Extract project & refpath
// <project>/raw/branch/file -> <project>, branch/file
project
:=
projectRe
.
FindString
(
r
.
Request
.
URL
.
Path
)
refpath
:=
r
.
Request
.
URL
.
Path
[
len
(
project
)
:
]
if
project
==
""
{
fail500
(
w
,
"extract project name"
,
nil
)
if
project
==
""
||
project
[
len
(
project
)
-
1
]
!=
'/'
{
fail500
(
w
,
"extract project name"
,
nil
)
// XXX err=nil
return
}
// assert project[-1] == "/"
project
=
project
[
:
len
(
project
)
-
1
]
refpath
:=
r
.
Request
.
URL
.
Path
[
len
(
project
)
:
]
if
refpath
[
:
4
]
!=
"raw/"
{
fail500
(
w
,
"refpath != raw/..."
,
nil
)
fail500
(
w
,
"refpath != raw/..."
,
nil
)
// XXX err=nil
return
}
project
=
project
[
:
len
(
project
)
-
1
]
refpath
=
refpath
[
4
:
]
// Query download access auth for this project
authReply
:=
verifyDownloadAccess
(
w
,
r
.
u
,
project
)
authReply
,
err
:=
verifyDownloadAccess
(
w
,
r
.
u
,
project
)
if
err
!=
nil
{
fail500
(
w
,
"verifyDownloadAccess"
,
err
)
return
}
if
authReply
.
RepoPath
==
""
{
// access denied - copy auth reply to client in full -
// there are HTTP code and other headers / body relevant for
...
...
@@ -192,10 +210,12 @@ Content-Transfer-Encoding: binary
Content-Type: text/plain; charset=utf-8
*/
// Emit content of blob located at <ref>/path (jointly denoted as 'refpath') to output
func
emitBlob
(
w
http
.
ResponseWriter
,
repopath
string
,
refpath
string
)
{
// Communicate with `git cat-file --batch` trying refs from longest
// to shortest prefix in refpath. This way we find longest-match for
// ref and get
object
content in the end.
// ref and get
blob sha1 and
content in the end.
queryCmd
:=
gitCommand
(
""
,
"git"
,
"--git-dir="
+
repopath
,
"cat-file"
,
"--batch"
)
queryStdin
,
err
:=
queryCmd
.
StdinPipe
()
if
err
!=
nil
{
...
...
@@ -216,7 +236,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
fail500
(
w
,
"git cat-file --batch; start"
,
err
)
return
}
defer
cleanUpProcessGroup
(
queryCmd
)
// XXX do we really need this?
defer
cleanUpProcessGroup
(
queryCmd
)
// refpath components as vector
refpathv
:=
strings
.
Split
(
refpath
,
"/"
)
...
...
@@ -224,9 +244,8 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
// scan from right to left and try to change '/' -> ':' and see if it
// creates a correct object name. If it does - we read object content
// which follows.
// TODO handle communication timeout
var
sha1
string
var
type_
string
// TODO handle communication timeout ?
var
sha1
,
type_
string
var
size
int64
for
i
:=
len
(
refpathv
);
i
>
0
;
i
--
{
ref
:=
strings
.
Join
(
refpathv
[
:
i
],
"/"
)
...
...
@@ -238,7 +257,7 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
return
}
reply
,
err
:=
queryReader
.
Read
Bytes
(
'\n'
)
reply
,
err
:=
queryReader
.
Read
String
(
'\n'
)
if
err
!=
nil
{
fail500
(
w
,
"git cat-file --batch; read"
,
err
)
return
...
...
@@ -247,12 +266,12 @@ func emitBlob(w http.ResponseWriter, repopath string, refpath string) {
log
.
Printf
(
"<- %s"
,
reply
)
// <object> SP missing LF
if
bytes
.
HasSuffix
(
reply
,
[]
byte
(
" missing
\n
"
))
{
// XXX byte literal?
if
bytes
.
HasSuffix
(
reply
,
" missing
\n
"
)
{
continue
}
// <sha1> SP <type> SP <size> LF
_
,
err
=
fmt
.
Sscanf
(
string
(
reply
)
,
"%s %s %d
\n
"
,
&
sha1
,
&
type_
,
&
size
)
_
,
err
=
fmt
.
Sscanf
(
reply
,
"%s %s %d
\n
"
,
&
sha1
,
&
type_
,
&
size
)
if
err
!=
nil
{
fail500
(
w
,
"git cat-file --batch; reply parse"
,
err
)
return
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment