Example #1
0
 public function releaseLock(RequestInterface $request)
 {
     if ($request->getMethod() !== 'PUT' || isset($_SERVER['HTTP_OC_CHUNKED'])) {
         return;
     }
     try {
         $node = $this->server->tree->getNodeForPath($request->getPath());
     } catch (NotFound $e) {
         return;
     }
     if ($node instanceof Node) {
         $node->releaseLock(ILockingProvider::LOCK_SHARED);
     }
 }
Example #2
0
 public function releaseLock(RequestInterface $request)
 {
     if ($request->getMethod() !== 'PUT') {
         return;
     }
     try {
         $node = $this->tree->getNodeForPath($request->getPath());
     } catch (NotFound $e) {
         return;
     }
     if ($node instanceof Node) {
         $node->releaseLock(ILockingProvider::LOCK_SHARED);
     }
 }
Example #3
0
    /**
     * This method checks the main HTTP preconditions.
     *
     * Currently these are:
     *   * If-Match
     *   * If-None-Match
     *   * If-Modified-Since
     *   * If-Unmodified-Since
     *
     * The method will return true if all preconditions are met
     * The method will return false, or throw an exception if preconditions
     * failed. If false is returned the operation should be aborted, and
     * the appropriate HTTP response headers are already set.
     *
     * Normally this method will throw 412 Precondition Failed for failures
     * related to If-None-Match, If-Match and If-Unmodified Since. It will
     * set the status to 304 Not Modified for If-Modified_since.
     *
     * @param RequestInterface $request
     * @param ResponseInterface $response
     * @return bool
     */
    function checkPreconditions(RequestInterface $request, ResponseInterface $response) {

        $path = $request->getPath();
        $node = null;
        $lastMod = null;
        $etag = null;

        if ($ifMatch = $request->getHeader('If-Match')) {

            // If-Match contains an entity tag. Only if the entity-tag
            // matches we are allowed to make the request succeed.
            // If the entity-tag is '*' we are only allowed to make the
            // request succeed if a resource exists at that url.
            try {
                $node = $this->tree->getNodeForPath($path);
            } catch (Exception\NotFound $e) {
                throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match');
            }

            // Only need to check entity tags if they are not *
            if ($ifMatch !== '*') {

                // There can be multiple ETags
                $ifMatch = explode(',', $ifMatch);
                $haveMatch = false;
                foreach ($ifMatch as $ifMatchItem) {

                    // Stripping any extra spaces
                    $ifMatchItem = trim($ifMatchItem, ' ');

                    $etag = $node instanceof IFile ? $node->getETag() : null;
                    if ($etag === $ifMatchItem) {
                        $haveMatch = true;
                    } else {
                        // Evolution has a bug where it sometimes prepends the "
                        // with a \. This is our workaround.
                        if (str_replace('\\"', '"', $ifMatchItem) === $etag) {
                            $haveMatch = true;
                        }
                    }

                }
                if (!$haveMatch) {
                    if ($etag) $response->setHeader('ETag', $etag);
                     throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match');
                }
            }
        }

        if ($ifNoneMatch = $request->getHeader('If-None-Match')) {

            // The If-None-Match header contains an ETag.
            // Only if the ETag does not match the current ETag, the request will succeed
            // The header can also contain *, in which case the request
            // will only succeed if the entity does not exist at all.
            $nodeExists = true;
            if (!$node) {
                try {
                    $node = $this->tree->getNodeForPath($path);
                } catch (Exception\NotFound $e) {
                    $nodeExists = false;
                }
            }
            if ($nodeExists) {
                $haveMatch = false;
                if ($ifNoneMatch === '*') $haveMatch = true;
                else {

                    // There might be multiple ETags
                    $ifNoneMatch = explode(',', $ifNoneMatch);
                    $etag = $node instanceof IFile ? $node->getETag() : null;

                    foreach ($ifNoneMatch as $ifNoneMatchItem) {

                        // Stripping any extra spaces
                        $ifNoneMatchItem = trim($ifNoneMatchItem, ' ');

                        if ($etag === $ifNoneMatchItem) $haveMatch = true;

                    }

                }

                if ($haveMatch) {
                    if ($etag) $response->setHeader('ETag', $etag);
                    if ($request->getMethod() === 'GET') {
                        $response->setStatus(304);
                        return false;
                    } else {
                        throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match');
                    }
                }
            }

        }

        if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) {

            // The If-Modified-Since header contains a date. We
            // will only return the entity if it has been changed since
            // that date. If it hasn't been changed, we return a 304
            // header
            // Note that this header only has to be checked if there was no If-None-Match header
            // as per the HTTP spec.
            $date = HTTP\Util::parseHTTPDate($ifModifiedSince);

            if ($date) {
                if (is_null($node)) {
                    $node = $this->tree->getNodeForPath($path);
                }
                $lastMod = $node->getLastModified();
                if ($lastMod) {
                    $lastMod = new \DateTime('@' . $lastMod);
                    if ($lastMod <= $date) {
                        $response->setStatus(304);
                        $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod));
                        return false;
                    }
                }
            }
        }

        if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) {

            // The If-Unmodified-Since will allow allow the request if the
            // entity has not changed since the specified date.
            $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince);

            // We must only check the date if it's valid
            if ($date) {
                if (is_null($node)) {
                    $node = $this->tree->getNodeForPath($path);
                }
                $lastMod = $node->getLastModified();
                if ($lastMod) {
                    $lastMod = new \DateTime('@' . $lastMod);
                    if ($lastMod > $date) {
                        throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since');
                    }
                }
            }

        }

        // Now the hardest, the If: header. The If: header can contain multiple
        // urls, ETags and so-called 'state tokens'.
        //
        // Examples of state tokens include lock-tokens (as defined in rfc4918)
        // and sync-tokens (as defined in rfc6578).
        //
        // The only proper way to deal with these, is to emit events, that a
        // Sync and Lock plugin can pick up.
        $ifConditions = $this->getIfConditions($request);

        foreach ($ifConditions as $kk => $ifCondition) {
            foreach ($ifCondition['tokens'] as $ii => $token) {
                $ifConditions[$kk]['tokens'][$ii]['validToken'] = false;
            }
        }

        // Plugins are responsible for validating all the tokens.
        // If a plugin deemed a token 'valid', it will set 'validToken' to
        // true.
        $this->emit('validateTokens', [ $request, &$ifConditions ]);

        // Now we're going to analyze the result.

        // Every ifCondition needs to validate to true, so we exit as soon as
        // we have an invalid condition.
        foreach ($ifConditions as $ifCondition) {

            $uri = $ifCondition['uri'];
            $tokens = $ifCondition['tokens'];

            // We only need 1 valid token for the condition to succeed.
            foreach ($tokens as $token) {

                $tokenValid = $token['validToken'] || !$token['token'];

                $etagValid = false;
                if (!$token['etag']) {
                    $etagValid = true;
                }
                // Checking the ETag, only if the token was already deamed
                // valid and there is one.
                if ($token['etag'] && $tokenValid) {

                    // The token was valid, and there was an ETag. We must
                    // grab the current ETag and check it.
                    $node = $this->tree->getNodeForPath($uri);
                    $etagValid = $node instanceof IFile && $node->getETag() == $token['etag'];

                }


                if (($tokenValid && $etagValid) ^ $token['negate']) {
                    // Both were valid, so we can go to the next condition.
                    continue 2;
                }


            }

            // If we ended here, it means there was no valid ETag + token
            // combination found for the current condition. This means we fail!
            throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If');

        }

        return true;

    }
 /**
  * This method is called before any HTTP method handler
  *
  * This method intercepts any GET, DELETE, PUT and PROPFIND calls to
  * filenames that are known to match the 'temporary file' regex.
  *
  * @param RequestInterface $request
  * @param ResponseInterface $response
  * @return bool
  */
 function beforeMethod(RequestInterface $request, ResponseInterface $response)
 {
     if (!($tempLocation = $this->isTempFile($request->getPath()))) {
         return;
     }
     switch ($request->getMethod()) {
         case 'GET':
             return $this->httpGet($request, $response, $tempLocation);
         case 'PUT':
             return $this->httpPut($request, $response, $tempLocation);
         case 'PROPFIND':
             return $this->httpPropfind($request, $response, $tempLocation);
         case 'DELETE':
             return $this->httpDelete($request, $response, $tempLocation);
     }
     return;
 }
Example #5
0
 /**
  * Triggered before any method is handled
  *
  * @param RequestInterface $request
  * @param ResponseInterface $response
  * @return void
  */
 function beforeMethod(RequestInterface $request, ResponseInterface $response)
 {
     $method = $request->getMethod();
     $path = $request->getPath();
     $exists = $this->server->tree->nodeExists($path);
     // If the node doesn't exists, none of these checks apply
     if (!$exists) {
         return;
     }
     switch ($method) {
         case 'GET':
         case 'HEAD':
         case 'OPTIONS':
             // For these 3 we only need to know if the node is readable.
             $this->checkPrivileges($path, '{DAV:}read');
             break;
         case 'PUT':
         case 'LOCK':
         case 'UNLOCK':
             // This method requires the write-content priv if the node
             // already exists, and bind on the parent if the node is being
             // created.
             // The bind privilege is handled in the beforeBind event.
             $this->checkPrivileges($path, '{DAV:}write-content');
             break;
         case 'PROPPATCH':
             $this->checkPrivileges($path, '{DAV:}write-properties');
             break;
         case 'ACL':
             $this->checkPrivileges($path, '{DAV:}write-acl');
             break;
         case 'COPY':
         case 'MOVE':
             // Copy requires read privileges on the entire source tree.
             // If the target exists write-content normally needs to be
             // checked, however, we're deleting the node beforehand and
             // creating a new one after, so this is handled by the
             // beforeUnbind event.
             //
             // The creation of the new node is handled by the beforeBind
             // event.
             //
             // If MOVE is used beforeUnbind will also be used to check if
             // the sourcenode can be deleted.
             $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE);
             break;
     }
 }
Example #6
0
 /**
  * The validateTokens event is triggered before every request.
  *
  * It's a moment where this plugin can check all the supplied lock tokens
  * in the If: header, and check if they are valid.
  *
  * In addition, it will also ensure that it checks any missing lokens that
  * must be present in the request, and reject requests without the proper
  * tokens.
  *
  * @param RequestInterface $request
  * @param mixed $conditions
  * @return void
  */
 function validateTokens(RequestInterface $request, &$conditions)
 {
     // First we need to gather a list of locks that must be satisfied.
     $mustLocks = [];
     $method = $request->getMethod();
     // Methods not in that list are operations that doesn't alter any
     // resources, and we don't need to check the lock-states for.
     switch ($method) {
         case 'DELETE':
             $mustLocks = array_merge($mustLocks, $this->getLocks($request->getPath(), true));
             break;
         case 'MKCOL':
         case 'MKCALENDAR':
         case 'PROPPATCH':
         case 'PUT':
         case 'PATCH':
             $mustLocks = array_merge($mustLocks, $this->getLocks($request->getPath(), false));
             break;
         case 'MOVE':
             $mustLocks = array_merge($mustLocks, $this->getLocks($request->getPath(), true));
             $mustLocks = array_merge($mustLocks, $this->getLocks($this->server->calculateUri($request->getHeader('Destination')), false));
             break;
         case 'COPY':
             $mustLocks = array_merge($mustLocks, $this->getLocks($this->server->calculateUri($request->getHeader('Destination')), false));
             break;
         case 'LOCK':
             //Temporary measure.. figure out later why this is needed
             // Here we basically ignore all incoming tokens...
             foreach ($conditions as $ii => $condition) {
                 foreach ($condition['tokens'] as $jj => $token) {
                     $conditions[$ii]['tokens'][$jj]['validToken'] = true;
                 }
             }
             return;
     }
     // It's possible that there's identical locks, because of shared
     // parents. We're removing the duplicates here.
     $tmp = [];
     foreach ($mustLocks as $lock) {
         $tmp[$lock->token] = $lock;
     }
     $mustLocks = array_values($tmp);
     foreach ($conditions as $kk => $condition) {
         foreach ($condition['tokens'] as $ii => $token) {
             // Lock tokens always start with opaquelocktoken:
             if (substr($token['token'], 0, 16) !== 'opaquelocktoken:') {
                 continue;
             }
             $checkToken = substr($token['token'], 16);
             // Looping through our list with locks.
             foreach ($mustLocks as $jj => $mustLock) {
                 if ($mustLock->token == $checkToken) {
                     // We have a match!
                     // Removing this one from mustlocks
                     unset($mustLocks[$jj]);
                     // Marking the condition as valid.
                     $conditions[$kk]['tokens'][$ii]['validToken'] = true;
                     // Advancing to the next token
                     continue 2;
                 }
             }
             // If we got here, it means that there was a
             // lock-token, but it was not in 'mustLocks'.
             //
             // This is an edge-case, as it could mean that token
             // was specified with a url that was not 'required' to
             // check. So we're doing one extra lookup to make sure
             // we really don't know this token.
             //
             // This also gets triggered when the user specified a
             // lock-token that was expired.
             $oddLocks = $this->getLocks($condition['uri']);
             foreach ($oddLocks as $oddLock) {
                 if ($oddLock->token === $checkToken) {
                     // We have a hit!
                     $conditions[$kk]['tokens'][$ii]['validToken'] = true;
                     continue 2;
                 }
             }
             // If we get all the way here, the lock-token was
             // really unknown.
         }
     }
     // If there's any locks left in the 'mustLocks' array, it means that
     // the resource was locked and we must block it.
     if ($mustLocks) {
         throw new DAV\Exception\Locked(reset($mustLocks));
     }
 }
Example #7
0
 function beforeMethod(RequestInterface $request, ResponseInterface $response)
 {
     $this->beforeMethod = $request->getMethod();
     return true;
 }
Example #8
0
 /**
  * Turns a RequestInterface object into an array with settings that can be
  * fed to curl_setopt
  *
  * @param RequestInterface $request
  * @return array
  */
 protected function createCurlSettingsArray(RequestInterface $request)
 {
     $settings = $this->curlSettings;
     switch ($request->getMethod()) {
         case 'HEAD':
             $settings[CURLOPT_NOBODY] = true;
             $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
             $settings[CURLOPT_POSTFIELDS] = '';
             $settings[CURLOPT_PUT] = false;
             break;
         case 'GET':
             $settings[CURLOPT_CUSTOMREQUEST] = 'GET';
             $settings[CURLOPT_POSTFIELDS] = '';
             $settings[CURLOPT_PUT] = false;
             break;
         default:
             $body = $request->getBody();
             if (is_resource($body)) {
                 // This needs to be set to PUT, regardless of the actual
                 // method used. Without it, INFILE will be ignored for some
                 // reason.
                 $settings[CURLOPT_PUT] = true;
                 $settings[CURLOPT_INFILE] = $request->getBody();
             } else {
                 // For security we cast this to a string. If somehow an array could
                 // be passed here, it would be possible for an attacker to use @ to
                 // post local files.
                 $settings[CURLOPT_POSTFIELDS] = (string) $body;
             }
             $settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
             break;
     }
     $nHeaders = [];
     foreach ($request->getHeaders() as $key => $values) {
         foreach ($values as $value) {
             $nHeaders[] = $key . ': ' . $value;
         }
     }
     $settings[CURLOPT_HTTPHEADER] = $nHeaders;
     $settings[CURLOPT_URL] = $request->getUrl();
     // FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM
     if (defined('CURLOPT_PROTOCOLS')) {
         $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
     }
     // FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM
     if (defined('CURLOPT_REDIR_PROTOCOLS')) {
         $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
     }
     return $settings;
 }