Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2895058
DiffusionServeController.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
15 KB
Referenced Files
None
Subscribers
None
DiffusionServeController.php
View Options
<?php
final
class
DiffusionServeController
extends
DiffusionController
{
public
static
function
isVCSRequest
(
AphrontRequest
$request
)
{
if
(
!
self
::
getCallsign
(
$request
)
)
{
return
null
;
}
$content_type
=
$request
->
getHTTPHeader
(
'Content-Type'
)
;
$user_agent
=
idx
(
$_SERVER
,
'HTTP_USER_AGENT'
)
;
$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
(
$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
;
}
private
static
function
getCallsign
(
AphrontRequest
$request
)
{
$uri
=
$request
->
getRequestURI
(
)
;
$regex
=
'@^/diffusion/(?P<callsign>[A-Z]+)(/|$)@'
;
$matches
=
null
;
if
(
!
preg_match
(
$regex
,
(string)
$uri
,
$matches
)
)
{
return
null
;
}
return
$matches
[
'callsign'
]
;
}
public
function
processRequest
(
)
{
$request
=
$this
->
getRequest
(
)
;
$callsign
=
self
::
getCallsign
(
$request
)
;
// If authentication credentials have been provided, try to find a user
// that actually matches those credentials.
if
(
isset
(
$_SERVER
[
'PHP_AUTH_USER'
]
)
&&
isset
(
$_SERVER
[
'PHP_AUTH_PW'
]
)
)
{
$username
=
$_SERVER
[
'PHP_AUTH_USER'
]
;
$password
=
new
PhutilOpaqueEnvelope
(
$_SERVER
[
'PHP_AUTH_PW'
]
)
;
$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
(
)
;
}
$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
)
->
withCallsigns
(
array
(
$callsign
)
)
->
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.'
)
)
;
}
}
}
if
(
!
$repository
->
isTracked
(
)
)
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository is inactive.'
)
)
;
}
$is_push
=
!
$this
->
isReadOnlyRequest
(
$repository
)
;
switch
(
$repository
->
getServeOverHTTP
(
)
)
{
case
PhabricatorRepository
::
SERVE_READONLY
:
if
(
$is_push
)
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository is read-only over HTTP.'
)
)
;
}
break
;
case
PhabricatorRepository
::
SERVE_READWRITE
:
if
(
$is_push
)
{
$can_push
=
PhabricatorPolicyFilter
::
hasCapability
(
$viewer
,
$repository
,
DiffusionCapabilityPush
::
CAPABILITY
)
;
if
(
!
$can_push
)
{
if
(
$viewer
->
isLoggedIn
(
)
)
{
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'You do not have permission to push to this repository.'
)
)
;
}
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.'
)
)
;
}
}
}
}
break
;
case
PhabricatorRepository
::
SERVE_OFF
:
default
:
return
new
PhabricatorVCSResponse
(
403
,
pht
(
'This repository is not available over HTTP.'
)
)
;
}
switch
(
$repository
->
getVersionControlSystem
(
)
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
$result
=
$this
->
serveGitRequest
(
$repository
,
$viewer
)
;
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
$result
=
$this
->
serveMercurialRequest
(
$repository
,
$viewer
)
;
break
;
default
:
$result
=
new
PhabricatorVCSResponse
(
999
,
pht
(
'TODO: Implement meaningful responses.'
)
)
;
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
isReadOnlyRequest
(
PhabricatorRepository
$repository
)
{
$request
=
$this
->
getRequest
(
)
;
$method
=
$_SERVER
[
'REQUEST_METHOD'
]
;
// TODO: This implementation is safe by default, but very incomplete.
switch
(
$repository
->
getVersionControlSystem
(
)
)
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
$service
=
$request
->
getStr
(
'service'
)
;
$path
=
$this
->
getRequestDirectoryPath
(
)
;
// 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'
)
{
// For "batch" we get a "cmds" argument like
//
// heads ;known nodes=
//
// We need to examine the commands (here, "heads" and "known") to
// make sure they're all read-only.
$args
=
$this
->
getMercurialArguments
(
)
;
$cmds
=
idx
(
$args
,
'cmds'
)
;
if
(
$cmds
)
{
// NOTE: Mercurial has some code to escape semicolons, but it does
// not actually function for command separation. For example, these
// two batch commands will produce completely different results (the
// former will run the lookup; the latter will fail with a parser
// error):
//
// lookup key=a:xb;lookup key=z* 0
// lookup key=a:;b;lookup key=z* 0
// ^
// |
// +-- Note semicolon.
//
// So just split unconditionally.
$cmds
=
explode
(
';'
,
$cmds
)
;
foreach
(
$cmds
as
$sub_cmd
)
{
$name
=
head
(
explode
(
' '
,
$sub_cmd
,
2
)
)
;
if
(
!
DiffusionMercurialWireProtocol
::
isReadOnlyCommand
(
$name
)
)
{
return
false
;
}
}
return
true
;
}
}
return
DiffusionMercurialWireProtocol
::
isReadOnlyCommand
(
$cmd
)
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SUBVERSION
:
break
;
}
return
false
;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
private
function
serveGitRequest
(
PhabricatorRepository
$repository
,
PhabricatorUser
$viewer
)
{
$request
=
$this
->
getRequest
(
)
;
$request_path
=
$this
->
getRequestDirectoryPath
(
)
;
$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
(
"Unable to find `git-http-backend` in PATH!"
)
;
}
$env
=
array
(
'REQUEST_METHOD'
=>
$_SERVER
[
'REQUEST_METHOD'
]
,
'QUERY_STRING'
=>
$query_string
,
'CONTENT_TYPE'
=>
$request
->
getHTTPHeader
(
'Content-Type'
)
,
'HTTP_CONTENT_ENCODING'
=>
$request
->
getHTTPHeader
(
'Content-Encoding'
)
,
'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
)
;
$input
=
PhabricatorStartup
::
getRawInput
(
)
;
list
(
$err
,
$stdout
,
$stderr
)
=
id
(
new
ExecFuture
(
'%s'
,
$bin
)
)
->
setEnv
(
$env
,
true
)
->
write
(
$input
)
->
resolve
(
)
;
if
(
$err
)
{
return
new
PhabricatorVCSResponse
(
500
,
pht
(
'Error %d: %s'
,
$err
,
$stderr
)
)
;
}
return
id
(
new
DiffusionGitResponse
(
)
)
->
setGitData
(
$stdout
)
;
}
private
function
getRequestDirectoryPath
(
)
{
$request
=
$this
->
getRequest
(
)
;
$request_path
=
$request
->
getRequestURI
(
)
->
getPath
(
)
;
return
preg_replace
(
'@^/diffusion/[A-Z]+@'
,
''
,
$request_path
)
;
}
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
;
}
$password_entry
=
id
(
new
PhabricatorRepositoryVCSPassword
(
)
)
->
loadOneWhere
(
'userPHID = %s'
,
$user
->
getPHID
(
)
)
;
if
(
!
$password_entry
)
{
// User doesn't have a password set.
return
null
;
}
if
(
!
$password_entry
->
comparePassword
(
$password
,
$user
)
)
{
// Password doesn't match.
return
null
;
}
if
(
$user
->
getIsDisabled
(
)
)
{
// User is disabled.
return
null
;
}
return
$user
;
}
private
function
serveMercurialRequest
(
PhabricatorRepository
$repository
)
{
$request
=
$this
->
getRequest
(
)
;
$bin
=
Filesystem
::
resolveBinary
(
'hg'
)
;
if
(
!
$bin
)
{
throw
new
Exception
(
"Unable to find `hg` in PATH!"
)
;
}
$env
=
array
(
)
;
$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"
;
}
list
(
$err
,
$stdout
,
$stderr
)
=
id
(
new
ExecFuture
(
'%s serve --stdio'
,
$bin
)
)
->
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
)
;
}
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
)
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Jan 19 2025, 20:53 (6 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128633
Default Alt Text
DiffusionServeController.php (15 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment