Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F97218996
DiffusionServeController.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Fri, Jan 3, 13:28
Size
36 KB
Mime Type
text/x-php
Expires
Sun, Jan 5, 13:28 (2 d)
Engine
blob
Format
Raw Data
Handle
23357144
Attached To
rPH Phabricator
DiffusionServeController.php
View Options
<?php
final
class
DiffusionServeController
extends
DiffusionController
{
private
$serviceViewer
;
private
$serviceRepository
;
private
$isGitLFSRequest
;
private
$gitLFSToken
;
private
$gitLFSInput
;
public
function
setServiceViewer
(
PhabricatorUser
$viewer
)
{
$this
->
getRequest
()->
setUser
(
$viewer
);
$this
->
serviceViewer
=
$viewer
;
return
$this
;
}
public
function
getServiceViewer
()
{
return
$this
->
serviceViewer
;
}
public
function
setServiceRepository
(
PhabricatorRepository
$repository
)
{
$this
->
serviceRepository
=
$repository
;
return
$this
;
}
public
function
getServiceRepository
()
{
return
$this
->
serviceRepository
;
}
public
function
getIsGitLFSRequest
()
{
return
$this
->
isGitLFSRequest
;
}
public
function
getGitLFSToken
()
{
return
$this
->
gitLFSToken
;
}
public
function
isVCSRequest
(
AphrontRequest
$request
)
{
$identifier
=
$this
->
getRepositoryIdentifierFromRequest
(
$request
);
if
(
$identifier
===
null
)
{
return
null
;
}
$content_type
=
$request
->
getHTTPHeader
(
'Content-Type'
);
$user_agent
=
idx
(
$_SERVER
,
'HTTP_USER_AGENT'
);
$request_type
=
$request
->
getHTTPHeader
(
'X-Phabricator-Request-Type'
);
// This may have a "charset" suffix, so only match the prefix.
$lfs_pattern
=
'(^application/vnd
\\
.git-lfs
\\
+json(;|
\z
))'
;
$vcs
=
null
;
if
(
$request
->
getExists
(
'service'
))
{
$service
=
$request
->
getStr
(
'service'
);
// We get this initially for `info/refs`.
// Git also gives us a User-Agent like "git/1.8.2.3".
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
}
else
if
(
strncmp
(
$user_agent
,
'git/'
,
4
)
===
0
)
{
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
}
else
if
(
$content_type
==
'application/x-git-upload-pack-request'
)
{
// We get this for `git-upload-pack`.
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
}
else
if
(
$content_type
==
'application/x-git-receive-pack-request'
)
{
// We get this for `git-receive-pack`.
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
}
else
if
(
preg_match
(
$lfs_pattern
,
$content_type
))
{
// This is a Git LFS HTTP API request.
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
$this
->
isGitLFSRequest
=
true
;
}
else
if
(
$request_type
==
'git-lfs'
)
{
// This is a Git LFS object content request.
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
$this
->
isGitLFSRequest
=
true
;
}
else
if
(
$request
->
getExists
(
'cmd'
))
{
// Mercurial also sends an Accept header like
// "application/mercurial-0.1", and a User-Agent like
// "mercurial/proto-1.0".
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
;
}
else
{
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
// serf/1.3.2".
$dav
=
$request
->
getHTTPHeader
(
'DAV'
);
$dav
=
new
PhutilURI
(
$dav
);
if
(
$dav
->
getDomain
()
===
'subversion.tigris.org'
)
{
$vcs
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
;
}
}
return
$vcs
;
}
public
function
handleRequest
(
AphrontRequest
$request
)
{
$service_exception
=
null
;
$response
=
null
;
try
{
$response
=
$this
->
serveRequest
(
$request
);
}
catch
(
Exception
$ex
)
{
$service_exception
=
$ex
;
}
try
{
$remote_addr
=
$request
->
getRemoteAddress
();
if
(
$request
->
isHTTPS
())
{
$remote_protocol
=
PhabricatorRepositoryPullEvent
::
PROTOCOL_HTTPS
;
}
else
{
$remote_protocol
=
PhabricatorRepositoryPullEvent
::
PROTOCOL_HTTP
;
}
$pull_event
=
id
(
new
PhabricatorRepositoryPullEvent
())
->
setEpoch
(
PhabricatorTime
::
getNow
())
->
setRemoteAddress
(
$remote_addr
)
->
setRemoteProtocol
(
$remote_protocol
);
if
(
$response
)
{
$response_code
=
$response
->
getHTTPResponseCode
();
if
(
$response_code
==
200
)
{
$pull_event
->
setResultType
(
PhabricatorRepositoryPullEvent
::
RESULT_PULL
)
->
setResultCode
(
$response_code
);
}
else
{
$pull_event
->
setResultType
(
PhabricatorRepositoryPullEvent
::
RESULT_ERROR
)
->
setResultCode
(
$response_code
);
}
if
(
$response
instanceof
PhabricatorVCSResponse
)
{
$pull_event
->
setProperties
(
array
(
'response.message'
=>
$response
->
getMessage
(),
));
}
}
else
{
$pull_event
->
setResultType
(
PhabricatorRepositoryPullEvent
::
RESULT_EXCEPTION
)
->
setResultCode
(
500
)
->
setProperties
(
array
(
'exception.class'
=>
get_class
(
$ex
),
'exception.message'
=>
$ex
->
getMessage
(),
));
}
$viewer
=
$this
->
getServiceViewer
();
if
(
$viewer
)
{
$pull_event
->
setPullerPHID
(
$viewer
->
getPHID
());
}
$repository
=
$this
->
getServiceRepository
();
if
(
$repository
)
{
$pull_event
->
setRepositoryPHID
(
$repository
->
getPHID
());
}
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
$pull_event
->
save
();
unset
(
$unguarded
);
}
catch
(
Exception
$ex
)
{
if
(
$service_exception
)
{
throw
$service_exception
;
}
throw
$ex
;
}
if
(
$service_exception
)
{
throw
$service_exception
;
}
return
$response
;
}
private
function
serveRequest
(
AphrontRequest
$request
)
{
$identifier
=
$this
->
getRepositoryIdentifierFromRequest
(
$request
);
// If authentication credentials have been provided, try to find a user
// that actually matches those credentials.
// We require both the username and password to be nonempty, because Git
// won't prompt users who provide a username but no password otherwise.
// See T10797 for discussion.
$have_user
=
strlen
(
idx
(
$_SERVER
,
'PHP_AUTH_USER'
));
$have_pass
=
strlen
(
idx
(
$_SERVER
,
'PHP_AUTH_PW'
));
if
(
$have_user
&&
$have_pass
)
{
$username
=
$_SERVER
[
'PHP_AUTH_USER'
];
$password
=
new
PhutilOpaqueEnvelope
(
$_SERVER
[
'PHP_AUTH_PW'
]);
// Try Git LFS auth first since we can usually reject it without doing
// any queries, since the username won't match the one we expect or the
// request won't be LFS.
$viewer
=
$this
->
authenticateGitLFSUser
(
$username
,
$password
);
// If that failed, try normal auth. Note that we can use normal auth on
// LFS requests, so this isn't strictly an alternative to LFS auth.
if
(!
$viewer
)
{
$viewer
=
$this
->
authenticateHTTPRepositoryUser
(
$username
,
$password
);
}
if
(!
$viewer
)
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'Invalid credentials.'
));
}
}
else
{
// User hasn't provided credentials, which means we count them as
// being "not logged in".
$viewer
=
new
PhabricatorUser
();
}
$this
->
setServiceViewer
(
$viewer
);
$allow_public
=
PhabricatorEnv
::
getEnvConfig
(
'policy.allow-public'
);
$allow_auth
=
PhabricatorEnv
::
getEnvConfig
(
'diffusion.allow-http-auth'
);
if
(!
$allow_public
)
{
if
(!
$viewer
->
isLoggedIn
())
{
if
(
$allow_auth
)
{
return
new
PhabricatorVCSResponse
(
401
,
pht
(
'You must log in to access repositories.'
));
}
else
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'Public and authenticated HTTP access are both forbidden.'
));
}
}
}
try
{
$repository
=
id
(
new
PhabricatorRepositoryQuery
())
->
setViewer
(
$viewer
)
->
withIdentifiers
(
array
(
$identifier
))
->
needURIs
(
true
)
->
executeOne
();
if
(!
$repository
)
{
return
new
PhabricatorVCSResponse
(
404
,
pht
(
'No such repository exists.'
));
}
}
catch
(
PhabricatorPolicyException
$ex
)
{
if
(
$viewer
->
isLoggedIn
())
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'You do not have permission to access this repository.'
));
}
else
{
if
(
$allow_auth
)
{
return
new
PhabricatorVCSResponse
(
401
,
pht
(
'You must log in to access this repository.'
));
}
else
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository requires authentication, which is forbidden '
.
'over HTTP.'
));
}
}
}
$response
=
$this
->
validateGitLFSRequest
(
$repository
,
$viewer
);
if
(
$response
)
{
return
$response
;
}
$this
->
setServiceRepository
(
$repository
);
if
(!
$repository
->
isTracked
())
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository is inactive.'
));
}
$is_push
=
!
$this
->
isReadOnlyRequest
(
$repository
);
if
(
$this
->
getIsGitLFSRequest
()
&&
$this
->
getGitLFSToken
())
{
// We allow git LFS requests over HTTP even if the repository does not
// otherwise support HTTP reads or writes, as long as the user is using a
// token from SSH. If they're using HTTP username + password auth, they
// have to obey the normal HTTP rules.
}
else
{
// For now, we don't distinguish between HTTP and HTTPS-originated
// requests that are proxied within the cluster, so the user can connect
// with HTTPS but we may be on HTTP by the time we reach this part of
// the code. Allow things to move forward as long as either protocol
// can be served.
$proto_https
=
PhabricatorRepositoryURI
::
BUILTIN_PROTOCOL_HTTPS
;
$proto_http
=
PhabricatorRepositoryURI
::
BUILTIN_PROTOCOL_HTTP
;
$can_read
=
$repository
->
canServeProtocol
(
$proto_https
,
false
)
||
$repository
->
canServeProtocol
(
$proto_http
,
false
);
if
(!
$can_read
)
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository is not available over HTTP.'
));
}
if
(
$is_push
)
{
$can_write
=
$repository
->
canServeProtocol
(
$proto_https
,
true
)
||
$repository
->
canServeProtocol
(
$proto_http
,
true
);
if
(!
$can_write
)
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository is read-only over HTTP.'
));
}
}
}
if
(
$is_push
)
{
$can_push
=
PhabricatorPolicyFilter
::
hasCapability
(
$viewer
,
$repository
,
DiffusionPushCapability
::
CAPABILITY
);
if
(!
$can_push
)
{
if
(
$viewer
->
isLoggedIn
())
{
$error_code
=
403
;
$error_message
=
pht
(
'You do not have permission to push to this repository ("%s").'
,
$repository
->
getDisplayName
());
if
(
$this
->
getIsGitLFSRequest
())
{
return
DiffusionGitLFSResponse
::
newErrorResponse
(
$error_code
,
$error_message
);
}
else
{
return
new
PhabricatorVCSResponse
(
$error_code
,
$error_message
);
}
}
else
{
if
(
$allow_auth
)
{
return
new
PhabricatorVCSResponse
(
401
,
pht
(
'You must log in to push to this repository.'
));
}
else
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'Pushing to this repository requires authentication, '
.
'which is forbidden over HTTP.'
));
}
}
}
}
$vcs_type
=
$repository
->
getVersionControlSystem
();
$req_type
=
$this
->
isVCSRequest
(
$request
);
if
(
$vcs_type
!=
$req_type
)
{
switch
(
$req_type
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
$result
=
new
PhabricatorVCSResponse
(
500
,
pht
(
'This repository ("%s") is not a Git repository.'
,
$repository
->
getDisplayName
()));
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
$result
=
new
PhabricatorVCSResponse
(
500
,
pht
(
'This repository ("%s") is not a Mercurial repository.'
,
$repository
->
getDisplayName
()));
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
$result
=
new
PhabricatorVCSResponse
(
500
,
pht
(
'This repository ("%s") is not a Subversion repository.'
,
$repository
->
getDisplayName
()));
break
;
default
:
$result
=
new
PhabricatorVCSResponse
(
500
,
pht
(
'Unknown request type.'
));
break
;
}
}
else
{
switch
(
$vcs_type
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
$result
=
$this
->
serveVCSRequest
(
$repository
,
$viewer
);
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
$result
=
new
PhabricatorVCSResponse
(
500
,
pht
(
'Phabricator does not support HTTP access to Subversion '
.
'repositories.'
));
break
;
default
:
$result
=
new
PhabricatorVCSResponse
(
500
,
pht
(
'Unknown version control system.'
));
break
;
}
}
$code
=
$result
->
getHTTPResponseCode
();
if
(
$is_push
&&
(
$code
==
200
))
{
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
$repository
->
writeStatusMessage
(
PhabricatorRepositoryStatusMessage
::
TYPE_NEEDS_UPDATE
,
PhabricatorRepositoryStatusMessage
::
CODE_OKAY
);
unset
(
$unguarded
);
}
return
$result
;
}
private
function
serveVCSRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
// We can serve Git LFS requests first, since we don't need to proxy them.
// It's also important that LFS requests never fall through to standard
// service pathways, because that would let you use LFS tokens to read
// normal repository data.
if
(
$this
->
getIsGitLFSRequest
())
{
return
$this
->
serveGitLFSRequest
(
$repository
,
$viewer
);
}
// If this repository is hosted on a service, we need to proxy the request
// to a host which can serve it.
$is_cluster_request
=
$this
->
getRequest
()->
isProxiedClusterRequest
();
$uri
=
$repository
->
getAlmanacServiceURI
(
$viewer
,
$is_cluster_request
,
array
(
'http'
,
'https'
,
));
if
(
$uri
)
{
$future
=
$this
->
getRequest
()->
newClusterProxyFuture
(
$uri
);
return
id
(
new
AphrontHTTPProxyResponse
())
->
setHTTPFuture
(
$future
);
}
// Otherwise, we're going to handle the request locally.
$vcs_type
=
$repository
->
getVersionControlSystem
();
switch
(
$vcs_type
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
$result
=
$this
->
serveGitRequest
(
$repository
,
$viewer
);
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
$result
=
$this
->
serveMercurialRequest
(
$repository
,
$viewer
);
break
;
}
return
$result
;
}
private
function
isReadOnlyRequest
(
PhabricatorRepository
$repository
)
{
$request
=
$this
->
getRequest
();
$method
=
$_SERVER
[
'REQUEST_METHOD'
];
// TODO: This implementation is safe by default, but very incomplete.
if
(
$this
->
getIsGitLFSRequest
())
{
return
$this
->
isGitLFSReadOnlyRequest
(
$repository
);
}
switch
(
$repository
->
getVersionControlSystem
())
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
$service
=
$request
->
getStr
(
'service'
);
$path
=
$this
->
getRequestDirectoryPath
(
$repository
);
// NOTE: Service names are the reverse of what you might expect, as they
// are from the point of view of the server. The main read service is
// "git-upload-pack", and the main write service is "git-receive-pack".
if
(
$method
==
'GET'
&&
$path
==
'/info/refs'
&&
$service
==
'git-upload-pack'
)
{
return
true
;
}
if
(
$path
==
'/git-upload-pack'
)
{
return
true
;
}
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
$cmd
=
$request
->
getStr
(
'cmd'
);
if
(
$cmd
==
'batch'
)
{
$cmds
=
idx
(
$this
->
getMercurialArguments
(),
'cmds'
);
return
DiffusionMercurialWireProtocol
::
isReadOnlyBatchCommand
(
$cmds
);
}
return
DiffusionMercurialWireProtocol
::
isReadOnlyCommand
(
$cmd
);
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
break
;
}
return
false
;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
private
function
serveGitRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
$request
=
$this
->
getRequest
();
$request_path
=
$this
->
getRequestDirectoryPath
(
$repository
);
$repository_root
=
$repository
->
getLocalPath
();
// Rebuild the query string to strip `__magic__` parameters and prevent
// issues where we might interpret inputs like "service=read&service=write"
// differently than the server does and pass it an unsafe command.
// NOTE: This does not use getPassthroughRequestParameters() because
// that code is HTTP-method agnostic and will encode POST data.
$query_data
=
$_GET
;
foreach
(
$query_data
as
$key
=>
$value
)
{
if
(!
strncmp
(
$key
,
'__'
,
2
))
{
unset
(
$query_data
[
$key
]);
}
}
$query_string
=
http_build_query
(
$query_data
,
''
,
'&'
);
// We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first.
$bin
=
Filesystem
::
resolveBinary
(
'git-http-backend'
);
if
(!
$bin
)
{
throw
new
Exception
(
pht
(
'Unable to find `%s` in %s!'
,
'git-http-backend'
,
'$PATH'
));
}
// NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already
// decompressed the request when we read the request body, so the body is
// just plain data with no encoding.
$env
=
array
(
'REQUEST_METHOD'
=>
$_SERVER
[
'REQUEST_METHOD'
],
'QUERY_STRING'
=>
$query_string
,
'CONTENT_TYPE'
=>
$request
->
getHTTPHeader
(
'Content-Type'
),
'REMOTE_ADDR'
=>
$_SERVER
[
'REMOTE_ADDR'
],
'GIT_PROJECT_ROOT'
=>
$repository_root
,
'GIT_HTTP_EXPORT_ALL'
=>
'1'
,
'PATH_INFO'
=>
$request_path
,
'REMOTE_USER'
=>
$viewer
->
getUsername
(),
// TODO: Set these correctly.
// GIT_COMMITTER_NAME
// GIT_COMMITTER_EMAIL
)
+
$this
->
getCommonEnvironment
(
$viewer
);
$input
=
PhabricatorStartup
::
getRawInput
();
$command
=
csprintf
(
'%s'
,
$bin
);
$command
=
PhabricatorDaemon
::
sudoCommandAsDaemonUser
(
$command
);
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
$cluster_engine
=
id
(
new
DiffusionRepositoryClusterEngine
())
->
setViewer
(
$viewer
)
->
setRepository
(
$repository
);
$did_write_lock
=
false
;
if
(
$this
->
isReadOnlyRequest
(
$repository
))
{
$cluster_engine
->
synchronizeWorkingCopyBeforeRead
();
}
else
{
$did_write_lock
=
true
;
$cluster_engine
->
synchronizeWorkingCopyBeforeWrite
();
}
$caught
=
null
;
try
{
list
(
$err
,
$stdout
,
$stderr
)
=
id
(
new
ExecFuture
(
'%C'
,
$command
))
->
setEnv
(
$env
,
true
)
->
write
(
$input
)
->
resolve
();
}
catch
(
Exception
$ex
)
{
$caught
=
$ex
;
}
if
(
$did_write_lock
)
{
$cluster_engine
->
synchronizeWorkingCopyAfterWrite
();
}
unset
(
$unguarded
);
if
(
$caught
)
{
throw
$caught
;
}
if
(
$err
)
{
if
(
$this
->
isValidGitShallowCloneResponse
(
$stdout
,
$stderr
))
{
// Ignore the error if the response passes this special check for
// validity.
$err
=
0
;
}
}
if
(
$err
)
{
return
new
PhabricatorVCSResponse
(
500
,
pht
(
'Error %d: %s'
,
$err
,
phutil_utf8ize
(
$stderr
)));
}
return
id
(
new
DiffusionGitResponse
())->
setGitData
(
$stdout
);
}
private
function
getRequestDirectoryPath
(
PhabricatorRepository
$repository
)
{
$request
=
$this
->
getRequest
();
$request_path
=
$request
->
getRequestURI
()->
getPath
();
$info
=
PhabricatorRepository
::
parseRepositoryServicePath
(
$request_path
,
$repository
->
getVersionControlSystem
());
$base_path
=
$info
[
'path'
];
// For Git repositories, strip an optional directory component if it
// isn't the name of a known Git resource. This allows users to clone
// repositories as "/diffusion/X/anything.git", for example.
if
(
$repository
->
isGit
())
{
$known
=
array
(
'info'
,
'git-upload-pack'
,
'git-receive-pack'
,
);
foreach
(
$known
as
$key
=>
$path
)
{
$known
[
$key
]
=
preg_quote
(
$path
,
'@'
);
}
$known
=
implode
(
'|'
,
$known
);
if
(
preg_match
(
'@^/([^/]+)/('
.
$known
.
')(/|$)@'
,
$base_path
))
{
$base_path
=
preg_replace
(
'@^/([^/]+)@'
,
''
,
$base_path
);
}
}
return
$base_path
;
}
private
function
authenticateGitLFSUser
(
$username
,
PhutilOpaqueEnvelope
$password
)
{
// Never accept these credentials for requests which aren't LFS requests.
if
(!
$this
->
getIsGitLFSRequest
())
{
return
null
;
}
// If we have the wrong username, don't bother checking if the token
// is right.
if
(
$username
!==
DiffusionGitLFSTemporaryTokenType
::
HTTP_USERNAME
)
{
return
null
;
}
$lfs_pass
=
$password
->
openEnvelope
();
$lfs_hash
=
PhabricatorHash
::
weakDigest
(
$lfs_pass
);
$token
=
id
(
new
PhabricatorAuthTemporaryTokenQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withTokenTypes
(
array
(
DiffusionGitLFSTemporaryTokenType
::
TOKENTYPE
))
->
withTokenCodes
(
array
(
$lfs_hash
))
->
withExpired
(
false
)
->
executeOne
();
if
(!
$token
)
{
return
null
;
}
$user
=
id
(
new
PhabricatorPeopleQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withPHIDs
(
array
(
$token
->
getUserPHID
()))
->
executeOne
();
if
(!
$user
)
{
return
null
;
}
if
(!
$user
->
isUserActivated
())
{
return
null
;
}
$this
->
gitLFSToken
=
$token
;
return
$user
;
}
private
function
authenticateHTTPRepositoryUser
(
$username
,
PhutilOpaqueEnvelope
$password
)
{
if
(!
PhabricatorEnv
::
getEnvConfig
(
'diffusion.allow-http-auth'
))
{
// No HTTP auth permitted.
return
null
;
}
if
(!
strlen
(
$username
))
{
// No username.
return
null
;
}
if
(!
strlen
(
$password
->
openEnvelope
()))
{
// No password.
return
null
;
}
$user
=
id
(
new
PhabricatorPeopleQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withUsernames
(
array
(
$username
))
->
executeOne
();
if
(!
$user
)
{
// Username doesn't match anything.
return
null
;
}
if
(!
$user
->
isUserActivated
())
{
// User is not activated.
return
null
;
}
$request
=
$this
->
getRequest
();
$content_source
=
PhabricatorContentSource
::
newFromRequest
(
$request
);
$engine
=
id
(
new
PhabricatorAuthPasswordEngine
())
->
setViewer
(
$user
)
->
setContentSource
(
$content_source
)
->
setPasswordType
(
PhabricatorAuthPassword
::
PASSWORD_TYPE_VCS
)
->
setObject
(
$user
);
if
(!
$engine
->
isValidPassword
(
$password
))
{
return
null
;
}
return
$user
;
}
private
function
serveMercurialRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
$request
=
$this
->
getRequest
();
$bin
=
Filesystem
::
resolveBinary
(
'hg'
);
if
(!
$bin
)
{
throw
new
Exception
(
pht
(
'Unable to find `%s` in %s!'
,
'hg'
,
'$PATH'
));
}
$env
=
$this
->
getCommonEnvironment
(
$viewer
);
$input
=
PhabricatorStartup
::
getRawInput
();
$cmd
=
$request
->
getStr
(
'cmd'
);
$args
=
$this
->
getMercurialArguments
();
$args
=
$this
->
formatMercurialArguments
(
$cmd
,
$args
);
if
(
strlen
(
$input
))
{
$input
=
strlen
(
$input
).
"
\n
"
.
$input
.
"0
\n
"
;
}
$command
=
csprintf
(
'%s -R %s serve --stdio'
,
$bin
,
$repository
->
getLocalPath
());
$command
=
PhabricatorDaemon
::
sudoCommandAsDaemonUser
(
$command
);
list
(
$err
,
$stdout
,
$stderr
)
=
id
(
new
ExecFuture
(
'%C'
,
$command
))
->
setEnv
(
$env
,
true
)
->
setCWD
(
$repository
->
getLocalPath
())
->
write
(
"{$cmd}
\n
{$args}{$input}"
)
->
resolve
();
if
(
$err
)
{
return
new
PhabricatorVCSResponse
(
500
,
pht
(
'Error %d: %s'
,
$err
,
$stderr
));
}
if
(
$cmd
==
'getbundle'
||
$cmd
==
'changegroup'
||
$cmd
==
'changegroupsubset'
)
{
// We're not completely sure that "changegroup" and "changegroupsubset"
// actually work, they're for very old Mercurial.
$body
=
gzcompress
(
$stdout
);
}
else
if
(
$cmd
==
'unbundle'
)
{
// This includes diagnostic information and anything echoed by commit
// hooks. We ignore `stdout` since it just has protocol garbage, and
// substitute `stderr`.
$body
=
strlen
(
$stderr
).
"
\n
"
.
$stderr
;
}
else
{
list
(
$length
,
$body
)
=
explode
(
"
\n
"
,
$stdout
,
2
);
if
(
$cmd
==
'capabilities'
)
{
$body
=
DiffusionMercurialWireProtocol
::
filterBundle2Capability
(
$body
);
}
}
return
id
(
new
DiffusionMercurialResponse
())->
setContent
(
$body
);
}
private
function
getMercurialArguments
()
{
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
// "Why would you do this?".
$args_raw
=
array
();
for
(
$ii
=
1
;;
$ii
++)
{
$header
=
'HTTP_X_HGARG_'
.
$ii
;
if
(!
array_key_exists
(
$header
,
$_SERVER
))
{
break
;
}
$args_raw
[]
=
$_SERVER
[
$header
];
}
$args_raw
=
implode
(
''
,
$args_raw
);
return
id
(
new
PhutilQueryStringParser
())
->
parseQueryString
(
$args_raw
);
}
private
function
formatMercurialArguments
(
$command
,
array
$arguments
)
{
$spec
=
DiffusionMercurialWireProtocol
::
getCommandArgs
(
$command
);
$out
=
array
();
// Mercurial takes normal arguments like this:
//
// name <length(value)>
// value
$has_star
=
false
;
foreach
(
$spec
as
$arg_key
)
{
if
(
$arg_key
==
'*'
)
{
$has_star
=
true
;
continue
;
}
if
(
isset
(
$arguments
[
$arg_key
]))
{
$value
=
$arguments
[
$arg_key
];
$size
=
strlen
(
$value
);
$out
[]
=
"{$arg_key} {$size}
\n
{$value}"
;
unset
(
$arguments
[
$arg_key
]);
}
}
if
(
$has_star
)
{
// Mercurial takes arguments for variable argument lists roughly like
// this:
//
// * <count(args)>
// argname1 <length(argvalue1)>
// argvalue1
// argname2 <length(argvalue2)>
// argvalue2
$count
=
count
(
$arguments
);
$out
[]
=
"* {$count}
\n
"
;
foreach
(
$arguments
as
$key
=>
$value
)
{
if
(
in_array
(
$key
,
$spec
))
{
// We already added this argument above, so skip it.
continue
;
}
$size
=
strlen
(
$value
);
$out
[]
=
"{$key} {$size}
\n
{$value}"
;
}
}
return
implode
(
''
,
$out
);
}
private
function
isValidGitShallowCloneResponse
(
$stdout
,
$stderr
)
{
// If you execute `git clone --depth N ...`, git sends a request which
// `git-http-backend` responds to by emitting valid output and then exiting
// with a failure code and an error message. If we ignore this error,
// everything works.
// This is a pretty funky fix: it would be nice to more precisely detect
// that a request is a `--depth N` clone request, but we don't have any code
// to decode protocol frames yet. Instead, look for reasonable evidence
// in the error and output that we're looking at a `--depth` clone.
// For evidence this isn't completely crazy, see:
// https://github.com/schacon/grack/pull/7
$stdout_regexp
=
'(^Content-Type: application/x-git-upload-pack-result)m'
;
$stderr_regexp
=
'(The remote end hung up unexpectedly)'
;
$has_pack
=
preg_match
(
$stdout_regexp
,
$stdout
);
$is_hangup
=
preg_match
(
$stderr_regexp
,
$stderr
);
return
$has_pack
&&
$is_hangup
;
}
private
function
getCommonEnvironment
(
PhabricatorUser
$viewer
)
{
$remote_address
=
$this
->
getRequest
()->
getRemoteAddress
();
return
array
(
DiffusionCommitHookEngine
::
ENV_USER
=>
$viewer
->
getUsername
(),
DiffusionCommitHookEngine
::
ENV_REMOTE_ADDRESS
=>
$remote_address
,
DiffusionCommitHookEngine
::
ENV_REMOTE_PROTOCOL
=>
'http'
,
);
}
private
function
validateGitLFSRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
if
(!
$this
->
getIsGitLFSRequest
())
{
return
null
;
}
if
(!
$repository
->
canUseGitLFS
())
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'The requested repository ("%s") does not support Git LFS.'
,
$repository
->
getDisplayName
()));
}
// If this is using an LFS token, sanity check that we're using it on the
// correct repository. This shouldn't really matter since the user could
// just request a proper token anyway, but it suspicious and should not
// be permitted.
$token
=
$this
->
getGitLFSToken
();
if
(
$token
)
{
$resource
=
$token
->
getTokenResource
();
if
(
$resource
!==
$repository
->
getPHID
())
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'The authentication token provided in the request is bound to '
.
'a different repository than the requested repository ("%s").'
,
$repository
->
getDisplayName
()));
}
}
return
null
;
}
private
function
serveGitLFSRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
if
(!
$this
->
getIsGitLFSRequest
())
{
throw
new
Exception
(
pht
(
'This is not a Git LFS request!'
));
}
$path
=
$this
->
getGitLFSRequestPath
(
$repository
);
$matches
=
null
;
if
(
preg_match
(
'(^upload/(.*)
\z
)'
,
$path
,
$matches
))
{
$oid
=
$matches
[
1
];
return
$this
->
serveGitLFSUploadRequest
(
$repository
,
$viewer
,
$oid
);
}
else
if
(
$path
==
'objects/batch'
)
{
return
$this
->
serveGitLFSBatchRequest
(
$repository
,
$viewer
);
}
else
{
return
DiffusionGitLFSResponse
::
newErrorResponse
(
404
,
pht
(
'Git LFS operation "%s" is not supported by this server.'
,
$path
));
}
}
private
function
serveGitLFSBatchRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
$input
=
$this
->
getGitLFSInput
();
$operation
=
idx
(
$input
,
'operation'
);
switch
(
$operation
)
{
case
'upload'
:
$want_upload
=
true
;
break
;
case
'download'
:
$want_upload
=
false
;
break
;
default
:
return
DiffusionGitLFSResponse
::
newErrorResponse
(
404
,
pht
(
'Git LFS batch operation "%s" is not supported by this server.'
,
$operation
));
}
$objects
=
idx
(
$input
,
'objects'
,
array
());
$hashes
=
array
();
foreach
(
$objects
as
$object
)
{
$hashes
[]
=
idx
(
$object
,
'oid'
);
}
if
(
$hashes
)
{
$refs
=
id
(
new
PhabricatorRepositoryGitLFSRefQuery
())
->
setViewer
(
$viewer
)
->
withRepositoryPHIDs
(
array
(
$repository
->
getPHID
()))
->
withObjectHashes
(
$hashes
)
->
execute
();
$refs
=
mpull
(
$refs
,
null
,
'getObjectHash'
);
}
else
{
$refs
=
array
();
}
$file_phids
=
mpull
(
$refs
,
'getFilePHID'
);
if
(
$file_phids
)
{
$files
=
id
(
new
PhabricatorFileQuery
())
->
setViewer
(
$viewer
)
->
withPHIDs
(
$file_phids
)
->
execute
();
$files
=
mpull
(
$files
,
null
,
'getPHID'
);
}
else
{
$files
=
array
();
}
$authorization
=
null
;
$output
=
array
();
foreach
(
$objects
as
$object
)
{
$oid
=
idx
(
$object
,
'oid'
);
$size
=
idx
(
$object
,
'size'
);
$ref
=
idx
(
$refs
,
$oid
);
$error
=
null
;
// NOTE: If we already have a ref for this object, we only emit a
// "download" action. The client should not upload the file again.
$actions
=
array
();
if
(
$ref
)
{
$file
=
idx
(
$files
,
$ref
->
getFilePHID
());
if
(
$file
)
{
// Git LFS may prompt users for authentication if the action does
// not provide an "Authorization" header and does not have a query
// parameter named "token". See here for discussion:
// <https://github.com/github/git-lfs/issues/1088>
$no_authorization
=
'Basic '
.
base64_encode
(
'none'
);
$get_uri
=
$file
->
getCDNURI
();
$actions
[
'download'
]
=
array
(
'href'
=>
$get_uri
,
'header'
=>
array
(
'Authorization'
=>
$no_authorization
,
'X-Phabricator-Request-Type'
=>
'git-lfs'
,
),
);
}
else
{
$error
=
array
(
'code'
=>
404
,
'message'
=>
pht
(
'Object "%s" was previously uploaded, but no longer exists '
.
'on this server.'
,
$oid
),
);
}
}
else
if
(
$want_upload
)
{
if
(!
$authorization
)
{
// Here, we could reuse the existing authorization if we have one,
// but it's a little simpler to just generate a new one
// unconditionally.
$authorization
=
$this
->
newGitLFSHTTPAuthorization
(
$repository
,
$viewer
,
$operation
);
}
$put_uri
=
$repository
->
getGitLFSURI
(
"info/lfs/upload/{$oid}"
);
$actions
[
'upload'
]
=
array
(
'href'
=>
$put_uri
,
'header'
=>
array
(
'Authorization'
=>
$authorization
,
'X-Phabricator-Request-Type'
=>
'git-lfs'
,
),
);
}
$object
=
array
(
'oid'
=>
$oid
,
'size'
=>
$size
,
);
if
(
$actions
)
{
$object
[
'actions'
]
=
$actions
;
}
if
(
$error
)
{
$object
[
'error'
]
=
$error
;
}
$output
[]
=
$object
;
}
$output
=
array
(
'objects'
=>
$output
,
);
return
id
(
new
DiffusionGitLFSResponse
())
->
setContent
(
$output
);
}
private
function
serveGitLFSUploadRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
,
$oid
)
{
$ref
=
id
(
new
PhabricatorRepositoryGitLFSRefQuery
())
->
setViewer
(
$viewer
)
->
withRepositoryPHIDs
(
array
(
$repository
->
getPHID
()))
->
withObjectHashes
(
array
(
$oid
))
->
executeOne
();
if
(
$ref
)
{
return
DiffusionGitLFSResponse
::
newErrorResponse
(
405
,
pht
(
'Content for object "%s" is already known to this server. It can '
.
'not be uploaded again.'
,
$oid
));
}
// Remove the execution time limit because uploading large files may take
// a while.
set_time_limit
(
0
);
$request_stream
=
new
AphrontRequestStream
();
$request_iterator
=
$request_stream
->
getIterator
();
$hashing_iterator
=
id
(
new
PhutilHashingIterator
(
$request_iterator
))
->
setAlgorithm
(
'sha256'
);
$source
=
id
(
new
PhabricatorIteratorFileUploadSource
())
->
setName
(
'lfs-'
.
$oid
)
->
setViewPolicy
(
PhabricatorPolicies
::
POLICY_NOONE
)
->
setIterator
(
$hashing_iterator
);
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
$file
=
$source
->
uploadFile
();
unset
(
$unguarded
);
$hash
=
$hashing_iterator
->
getHash
();
if
(
$hash
!==
$oid
)
{
return
DiffusionGitLFSResponse
::
newErrorResponse
(
400
,
pht
(
'Uploaded data is corrupt or invalid. Expected hash "%s", actual '
.
'hash "%s".'
,
$oid
,
$hash
));
}
$ref
=
id
(
new
PhabricatorRepositoryGitLFSRef
())
->
setRepositoryPHID
(
$repository
->
getPHID
())
->
setObjectHash
(
$hash
)
->
setByteSize
(
$file
->
getByteSize
())
->
setAuthorPHID
(
$viewer
->
getPHID
())
->
setFilePHID
(
$file
->
getPHID
());
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
// Attach the file to the repository to give users permission
// to access it.
$file
->
attachToObject
(
$repository
->
getPHID
());
$ref
->
save
();
unset
(
$unguarded
);
// This is just a plain HTTP 200 with no content, which is what `git lfs`
// expects.
return
new
DiffusionGitLFSResponse
();
}
private
function
newGitLFSHTTPAuthorization
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
,
$operation
)
{
$unguarded
=
AphrontWriteGuard
::
beginScopedUnguardedWrites
();
$authorization
=
DiffusionGitLFSTemporaryTokenType
::
newHTTPAuthorization
(
$repository
,
$viewer
,
$operation
);
unset
(
$unguarded
);
return
$authorization
;
}
private
function
getGitLFSRequestPath
(
PhabricatorRepository
$repository
)
{
$request_path
=
$this
->
getRequestDirectoryPath
(
$repository
);
$matches
=
null
;
if
(
preg_match
(
'(^/info/lfs(?:
\z
|/)(.*))'
,
$request_path
,
$matches
))
{
return
$matches
[
1
];
}
return
null
;
}
private
function
getGitLFSInput
()
{
if
(!
$this
->
gitLFSInput
)
{
$input
=
PhabricatorStartup
::
getRawInput
();
$input
=
phutil_json_decode
(
$input
);
$this
->
gitLFSInput
=
$input
;
}
return
$this
->
gitLFSInput
;
}
private
function
isGitLFSReadOnlyRequest
(
PhabricatorRepository
$repository
)
{
if
(!
$this
->
getIsGitLFSRequest
())
{
return
false
;
}
$path
=
$this
->
getGitLFSRequestPath
(
$repository
);
if
(
$path
===
'objects/batch'
)
{
$input
=
$this
->
getGitLFSInput
();
$operation
=
idx
(
$input
,
'operation'
);
switch
(
$operation
)
{
case
'download'
:
return
true
;
default
:
return
false
;
}
}
return
false
;
}
}
Event Timeline
Log In to Comment