Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F92645001
DiffusionSubversionServeSSHWorkflow.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, Nov 22, 08:31
Size
13 KB
Mime Type
text/x-php
Expires
Sun, Nov 24, 08:31 (2 d)
Engine
blob
Format
Raw Data
Handle
22476856
Attached To
rPH Phabricator
DiffusionSubversionServeSSHWorkflow.php
View Options
<?php
/**
* This protocol has a good spec here:
*
* http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
*/
final
class
DiffusionSubversionServeSSHWorkflow
extends
DiffusionSubversionSSHWorkflow
{
private
$didSeeWrite
;
private
$inProtocol
;
private
$outProtocol
;
private
$inSeenGreeting
;
private
$outPhaseCount
=
0
;
private
$internalBaseURI
;
private
$externalBaseURI
;
private
$peekBuffer
;
private
$command
;
private
$isProxying
;
private
function
getCommand
()
{
return
$this
->
command
;
}
protected
function
didConstruct
()
{
$this
->
setName
(
'svnserve'
);
$this
->
setArguments
(
array
(
array
(
'name'
=>
'tunnel'
,
'short'
=>
't'
,
),
));
}
protected
function
identifyRepository
()
{
// NOTE: In SVN, we need to read the first few protocol frames before we
// can determine which repository the user is trying to access. We're
// going to peek at the data on the wire to identify the repository.
$io_channel
=
$this
->
getIOChannel
();
// Before the client will send us the first protocol frame, we need to send
// it a connection frame with server capabilities. To figure out the
// correct frame we're going to start `svnserve`, read the frame from it,
// send it to the client, then kill the subprocess.
// TODO: This is pretty inelegant and the protocol frame will change very
// rarely. We could cache it if we can find a reasonable way to dirty the
// cache.
$command
=
csprintf
(
'svnserve -t'
);
$command
=
PhabricatorDaemon
::
sudoCommandAsDaemonUser
(
$command
);
$future
=
new
ExecFuture
(
'%C'
,
$command
);
$exec_channel
=
new
PhutilExecChannel
(
$future
);
$exec_protocol
=
new
DiffusionSubversionWireProtocol
();
while
(
true
)
{
PhutilChannel
::
waitForAny
(
array
(
$exec_channel
));
$exec_channel
->
update
();
$exec_message
=
$exec_channel
->
read
();
if
(
$exec_message
!==
null
)
{
$messages
=
$exec_protocol
->
writeData
(
$exec_message
);
if
(
$messages
)
{
$message
=
head
(
$messages
);
$raw
=
$message
[
'raw'
];
// Write the greeting frame to the client.
$io_channel
->
write
(
$raw
);
// Kill the subprocess.
$future
->
resolveKill
();
break
;
}
}
if
(!
$exec_channel
->
isOpenForReading
())
{
throw
new
Exception
(
pht
(
'%s subprocess exited before emitting a protocol frame.'
,
'svnserve'
));
}
}
$io_protocol
=
new
DiffusionSubversionWireProtocol
();
while
(
true
)
{
PhutilChannel
::
waitForAny
(
array
(
$io_channel
));
$io_channel
->
update
();
$in_message
=
$io_channel
->
read
();
if
(
$in_message
!==
null
)
{
$this
->
peekBuffer
.=
$in_message
;
if
(
strlen
(
$this
->
peekBuffer
)
>
(
1024
*
1024
))
{
throw
new
Exception
(
pht
(
'Client transmitted more than 1MB of data without transmitting '
.
'a recognizable protocol frame.'
));
}
$messages
=
$io_protocol
->
writeData
(
$in_message
);
if
(
$messages
)
{
$message
=
head
(
$messages
);
$struct
=
$message
[
'structure'
];
// This is the:
//
// ( version ( cap1 ... ) url ... )
//
// The `url` allows us to identify the repository.
$uri
=
$struct
[
2
][
'value'
];
$path
=
$this
->
getPathFromSubversionURI
(
$uri
);
return
$this
->
loadRepositoryWithPath
(
$path
,
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
);
}
}
if
(!
$io_channel
->
isOpenForReading
())
{
throw
new
Exception
(
pht
(
'Client closed connection before sending a complete protocol '
.
'frame.'
));
}
// If the client has disconnected, kill the subprocess and bail.
if
(!
$io_channel
->
isOpenForWriting
())
{
throw
new
Exception
(
pht
(
'Client closed connection before receiving response.'
));
}
}
}
protected
function
executeRepositoryOperations
()
{
$repository
=
$this
->
getRepository
();
$args
=
$this
->
getArgs
();
if
(!
$args
->
getArg
(
'tunnel'
))
{
throw
new
Exception
(
pht
(
'Expected `%s`!'
,
'svnserve -t'
));
}
if
(
$this
->
shouldProxy
())
{
$command
=
$this
->
getProxyCommand
();
$this
->
isProxying
=
true
;
$cwd
=
null
;
}
else
{
$command
=
csprintf
(
'svnserve -t --tunnel-user=%s'
,
$this
->
getUser
()->
getUsername
());
$cwd
=
PhabricatorEnv
::
getEmptyCWD
();
}
$command
=
PhabricatorDaemon
::
sudoCommandAsDaemonUser
(
$command
);
$future
=
new
ExecFuture
(
'%C'
,
$command
);
// If we're receiving a commit, svnserve will fail to execute the commit
// hook with an unhelpful error if the CWD isn't readable by the user we
// are sudoing to. Switch to a readable, empty CWD before running
// svnserve. See T10941.
if
(
$cwd
!==
null
)
{
$future
->
setCWD
(
$cwd
);
}
$this
->
inProtocol
=
new
DiffusionSubversionWireProtocol
();
$this
->
outProtocol
=
new
DiffusionSubversionWireProtocol
();
$this
->
command
=
id
(
$this
->
newPassthruCommand
())
->
setIOChannel
(
$this
->
getIOChannel
())
->
setCommandChannelFromExecFuture
(
$future
)
->
setWillWriteCallback
(
array
(
$this
,
'willWriteMessageCallback'
))
->
setWillReadCallback
(
array
(
$this
,
'willReadMessageCallback'
));
$this
->
command
->
setPauseIOReads
(
true
);
$err
=
$this
->
command
->
execute
();
if
(!
$err
&&
$this
->
didSeeWrite
)
{
$this
->
getRepository
()->
writeStatusMessage
(
PhabricatorRepositoryStatusMessage
::
TYPE_NEEDS_UPDATE
,
PhabricatorRepositoryStatusMessage
::
CODE_OKAY
);
}
return
$err
;
}
public
function
willWriteMessageCallback
(
PhabricatorSSHPassthruCommand
$command
,
$message
)
{
$proto
=
$this
->
inProtocol
;
$messages
=
$proto
->
writeData
(
$message
);
$result
=
array
();
foreach
(
$messages
as
$message
)
{
$message_raw
=
$message
[
'raw'
];
$struct
=
$message
[
'structure'
];
if
(!
$this
->
inSeenGreeting
)
{
$this
->
inSeenGreeting
=
true
;
// The first message the client sends looks like:
//
// ( version ( cap1 ... ) url ... )
//
// We want to grab the URL, load the repository, make sure it exists and
// is accessible, and then replace it with the location of the
// repository on disk.
$uri
=
$struct
[
2
][
'value'
];
$struct
[
2
][
'value'
]
=
$this
->
makeInternalURI
(
$uri
);
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
}
else
if
(
isset
(
$struct
[
0
])
&&
$struct
[
0
][
'type'
]
==
'word'
)
{
if
(!
$proto
->
isReadOnlyCommand
(
$struct
))
{
$this
->
didSeeWrite
=
true
;
$this
->
requireWriteAccess
(
$struct
[
0
][
'value'
]);
}
// Several other commands also pass in URLs. We need to translate
// all of these into the internal representation; this also makes sure
// they're valid and accessible.
switch
(
$struct
[
0
][
'value'
])
{
case
'reparent'
:
// ( reparent ( url ) )
$struct
[
1
][
'value'
][
0
][
'value'
]
=
$this
->
makeInternalURI
(
$struct
[
1
][
'value'
][
0
][
'value'
]);
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
break
;
case
'switch'
:
// ( switch ( ( rev ) target recurse url ... ) )
$struct
[
1
][
'value'
][
3
][
'value'
]
=
$this
->
makeInternalURI
(
$struct
[
1
][
'value'
][
3
][
'value'
]);
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
break
;
case
'diff'
:
// ( diff ( ( rev ) target recurse ignore-ancestry url ... ) )
$struct
[
1
][
'value'
][
4
][
'value'
]
=
$this
->
makeInternalURI
(
$struct
[
1
][
'value'
][
4
][
'value'
]);
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
break
;
case
'add-file'
:
case
'add-dir'
:
// ( add-file ( path dir-token file-token [ copy-path copy-rev ] ) )
// ( add-dir ( path parent child [ copy-path copy-rev ] ) )
if
(
isset
(
$struct
[
1
][
'value'
][
3
][
'value'
][
0
][
'value'
]))
{
$copy_from
=
$struct
[
1
][
'value'
][
3
][
'value'
][
0
][
'value'
];
$copy_from
=
$this
->
makeInternalURI
(
$copy_from
);
$struct
[
1
][
'value'
][
3
][
'value'
][
0
][
'value'
]
=
$copy_from
;
}
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
break
;
}
}
$result
[]
=
$message_raw
;
}
if
(!
$result
)
{
return
null
;
}
return
implode
(
''
,
$result
);
}
public
function
willReadMessageCallback
(
PhabricatorSSHPassthruCommand
$command
,
$message
)
{
$proto
=
$this
->
outProtocol
;
$messages
=
$proto
->
writeData
(
$message
);
$result
=
array
();
foreach
(
$messages
as
$message
)
{
$message_raw
=
$message
[
'raw'
];
$struct
=
$message
[
'structure'
];
if
(
isset
(
$struct
[
0
])
&&
(
$struct
[
0
][
'type'
]
==
'word'
))
{
if
(
$struct
[
0
][
'value'
]
==
'success'
)
{
switch
(
$this
->
outPhaseCount
)
{
case
0
:
// This is the "greeting", which announces capabilities.
// We already sent this when we were figuring out which
// repository this request is for, so we aren't going to send
// it again.
// Instead, we're going to replay the client's response (which
// we also already read).
$command
=
$this
->
getCommand
();
$command
->
writeIORead
(
$this
->
peekBuffer
);
$command
->
setPauseIOReads
(
false
);
$message_raw
=
null
;
break
;
case
1
:
// This responds to the client greeting, and announces auth.
break
;
case
2
:
// This responds to auth, which should be trivial over SSH.
break
;
case
3
:
// This contains the URI of the repository. We need to edit it;
// if it does not match what the client requested it will reject
// the response.
$struct
[
1
][
'value'
][
1
][
'value'
]
=
$this
->
makeExternalURI
(
$struct
[
1
][
'value'
][
1
][
'value'
]);
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
break
;
default
:
// We don't care about other protocol frames.
break
;
}
$this
->
outPhaseCount
++;
}
else
if
(
$struct
[
0
][
'value'
]
==
'failure'
)
{
// Find any error messages which include the internal URI, and
// replace the text with the external URI.
foreach
(
$struct
[
1
][
'value'
]
as
$key
=>
$error
)
{
$code
=
$error
[
'value'
][
0
][
'value'
];
$message
=
$error
[
'value'
][
1
][
'value'
];
$message
=
str_replace
(
$this
->
internalBaseURI
,
$this
->
externalBaseURI
,
$message
);
// Derp derp derp derp derp. The structure looks like this:
// ( failure ( ( code message ... ) ... ) )
$struct
[
1
][
'value'
][
$key
][
'value'
][
1
][
'value'
]
=
$message
;
}
$message_raw
=
$proto
->
serializeStruct
(
$struct
);
}
}
if
(
$message_raw
!==
null
)
{
$result
[]
=
$message_raw
;
}
}
if
(!
$result
)
{
return
null
;
}
return
implode
(
''
,
$result
);
}
private
function
getPathFromSubversionURI
(
$uri_string
)
{
$uri
=
new
PhutilURI
(
$uri_string
);
$proto
=
$uri
->
getProtocol
();
if
(
$proto
!==
'svn+ssh'
)
{
throw
new
Exception
(
pht
(
'Protocol for URI "%s" MUST be "%s".'
,
$uri_string
,
'svn+ssh'
));
}
$path
=
$uri
->
getPath
();
// Subversion presumably deals with this, but make sure there's nothing
// sketchy going on with the URI.
if
(
preg_match
(
'(/
\\
.
\\
./)'
,
$path
))
{
throw
new
Exception
(
pht
(
'String "%s" is invalid in path specification "%s".'
,
'/../'
,
$uri_string
));
}
$path
=
$this
->
normalizeSVNPath
(
$path
);
return
$path
;
}
private
function
makeInternalURI
(
$uri_string
)
{
if
(
$this
->
isProxying
)
{
return
$uri_string
;
}
$uri
=
new
PhutilURI
(
$uri_string
);
$repository
=
$this
->
getRepository
();
$path
=
$this
->
getPathFromSubversionURI
(
$uri_string
);
$external_base
=
$this
->
getBaseRequestPath
();
// Replace "/diffusion/X" in the request with the repository local path,
// so "/diffusion/X/master/" becomes "/path/to/repository/X/master/".
$local_path
=
rtrim
(
$repository
->
getLocalPath
(),
'/'
);
$path
=
$local_path
.
substr
(
$path
,
strlen
(
$external_base
));
// NOTE: We are intentionally NOT removing username information from the
// URI. Subversion retains it over the course of the request and considers
// two repositories with different username identifiers to be distinct and
// incompatible.
$uri
->
setPath
(
$path
);
// If this is happening during the handshake, these are the base URIs for
// the request.
if
(
$this
->
externalBaseURI
===
null
)
{
$pre
=
(
string
)
id
(
clone
$uri
)->
setPath
(
''
);
$external_path
=
$external_base
;
$external_path
=
$this
->
normalizeSVNPath
(
$external_path
);
$this
->
externalBaseURI
=
$pre
.
$external_path
;
$internal_path
=
rtrim
(
$repository
->
getLocalPath
(),
'/'
);
$internal_path
=
$this
->
normalizeSVNPath
(
$internal_path
);
$this
->
internalBaseURI
=
$pre
.
$internal_path
;
}
return
(
string
)
$uri
;
}
private
function
makeExternalURI
(
$uri
)
{
if
(
$this
->
isProxying
)
{
return
$uri
;
}
$internal
=
$this
->
internalBaseURI
;
$external
=
$this
->
externalBaseURI
;
if
(
strncmp
(
$uri
,
$internal
,
strlen
(
$internal
))
===
0
)
{
$uri
=
$external
.
substr
(
$uri
,
strlen
(
$internal
));
}
return
$uri
;
}
private
function
normalizeSVNPath
(
$path
)
{
// Subversion normalizes redundant slashes internally, so normalize them
// here as well to make sure things match up.
$path
=
preg_replace
(
'(/+)'
,
'/'
,
$path
);
return
$path
;
}
}
Event Timeline
Log In to Comment