3232use OCP \IUserManager ;
3333use OCP \IUserSession ;
3434use OCP \L10N \IFactory ;
35+ use OCP \Security \IRemoteHostValidator ;
3536use OCP \Server ;
3637use OCP \TaskProcessing \EShapeType ;
3738use OCP \TaskProcessing \Events \GetTaskProcessingProvidersEvent ;
@@ -767,6 +768,12 @@ class TaskProcessingTest extends \Test\TestCase {
767768 private IJobList &MockObject $ jobList ;
768769 private IUserMountCache &MockObject $ userMountCache ;
769770 private RegistrationContext &MockObject $ registrationContext ;
771+ private IRemoteHostValidator &MockObject $ remoteHostValidator ;
772+
773+ /** @var list<string> hosts the mocked IRemoteHostValidator rejects */
774+ private array $ invalidRemoteHosts = [];
775+ /** Makes the mocked IRemoteHostValidator reject every host */
776+ private bool $ rejectAllRemoteHosts = false ;
770777
771778 /** @var array<class-string, IProvider> */
772779 private array $ providers ;
@@ -835,6 +842,11 @@ protected function setUp(): void {
835842 );
836843
837844 $ this ->userMountCache = $ this ->createMock (IUserMountCache::class);
845+ $ this ->invalidRemoteHosts = [];
846+ $ this ->rejectAllRemoteHosts = false ;
847+ $ this ->remoteHostValidator = $ this ->createMock (IRemoteHostValidator::class);
848+ $ this ->remoteHostValidator ->expects ($ this ->any ())->method ('isValid ' )
849+ ->willReturnCallback (fn (string $ host ): bool => !$ this ->rejectAllRemoteHosts && !in_array ($ host , $ this ->invalidRemoteHosts , true ));
838850 $ this ->config = Server::get (IConfig::class);
839851 $ this ->appConfig = Server::get (IAppConfig::class);
840852 $ this ->manager = new Manager (
@@ -856,6 +868,7 @@ protected function setUp(): void {
856868 Server::get (ICacheFactory::class),
857869 Server::get (IFactory::class),
858870 Server::get (ITimeFactory::class),
871+ $ this ->remoteHostValidator ,
859872 );
860873 }
861874
@@ -905,6 +918,106 @@ public function testProviderShouldBeRegisteredAndTaskFailValidation(): void {
905918 $ this ->manager ->scheduleTask ($ task );
906919 }
907920
921+ public static function invalidWebhookDataProvider (): array {
922+ return [
923+ 'uri without method ' => ['https://example.com/hook ' , null ],
924+ 'method without uri ' => [null , 'HTTP:POST ' ],
925+ 'empty uri with method ' => ['' , 'HTTP:POST ' ],
926+ 'uri with empty method ' => ['https://example.com/hook ' , '' ],
927+ 'unknown method prefix ' => ['https://example.com/hook ' , 'FTP:GET ' ],
928+ 'unknown http verb ' => ['https://example.com/hook ' , 'HTTP:PATCH ' ],
929+ 'lowercase http verb ' => ['https://example.com/hook ' , 'HTTP:post ' ],
930+ 'unsupported uri scheme ' => ['file:///etc/passwd ' , 'HTTP:GET ' ],
931+ 'relative uri for http method ' => ['/some/path ' , 'HTTP:POST ' ],
932+ 'malformed uri ' => ['https:// ' , 'HTTP:POST ' ],
933+ 'appapi method without exapp id ' => ['/some/path ' , 'AppAPI:POST ' ],
934+ 'appapi method with too many parts ' => ['/some/path ' , 'AppAPI:my_app:POST:extra ' ],
935+ 'appapi method with invalid exapp id ' => ['/some/path ' , 'AppAPI:My App:POST ' ],
936+ 'appapi method with unknown http verb ' => ['/some/path ' , 'AppAPI:my_app:PATCH ' ],
937+ 'absolute uri for appapi method ' => ['https://example.com/hook ' , 'AppAPI:my_app:POST ' ],
938+ 'uri too long ' => ['https://example.com/ ' . str_repeat ('a ' , 4000 ), 'HTTP:POST ' ],
939+ 'method too long ' => ['/some/path ' , 'AppAPI: ' . str_repeat ('a ' , 64 ) . ':POST ' ],
940+ ];
941+ }
942+
943+ #[\PHPUnit \Framework \Attributes \DataProvider('invalidWebhookDataProvider ' )]
944+ public function testProviderShouldBeRegisteredAndWebhookFailValidation (?string $ webhookUri , ?string $ webhookMethod ): void {
945+ $ this ->registrationContext ->expects ($ this ->any ())->method ('getTaskProcessingProviders ' )->willReturn ([
946+ new ServiceRegistration ('test ' , SuccessfulSyncProvider::class)
947+ ]);
948+ $ task = new Task (TextToText::ID , ['input ' => 'Hello ' ], 'test ' , null );
949+ $ task ->setWebhookUri ($ webhookUri );
950+ $ task ->setWebhookMethod ($ webhookMethod );
951+ self ::expectException (ValidationException::class);
952+ $ this ->manager ->scheduleTask ($ task );
953+ }
954+
955+ public static function validWebhookDataProvider (): array {
956+ return [
957+ 'no webhook ' => [null , null ],
958+ 'empty webhook ' => ['' , '' ],
959+ 'http get ' => ['http://example.com/hook ' , 'HTTP:GET ' ],
960+ 'https post ' => ['https://example.com/hook?foo=bar ' , 'HTTP:POST ' ],
961+ 'https put ' => ['https://example.com/hook ' , 'HTTP:PUT ' ],
962+ 'https delete ' => ['https://example.com/hook ' , 'HTTP:DELETE ' ],
963+ 'appapi post ' => ['/some/path ' , 'AppAPI:my_app:POST ' ],
964+ 'appapi get ' => ['/ ' , 'AppAPI:my-app2:GET ' ],
965+ ];
966+ }
967+
968+ #[\PHPUnit \Framework \Attributes \DataProvider('validWebhookDataProvider ' )]
969+ public function testProviderShouldBeRegisteredAndWebhookPassValidation (?string $ webhookUri , ?string $ webhookMethod ): void {
970+ $ this ->registrationContext ->expects ($ this ->any ())->method ('getTaskProcessingProviders ' )->willReturn ([
971+ new ServiceRegistration ('test ' , SuccessfulSyncProvider::class)
972+ ]);
973+ $ task = new Task (TextToText::ID , ['input ' => 'Hello ' ], 'test ' , null );
974+ $ task ->setWebhookUri ($ webhookUri );
975+ $ task ->setWebhookMethod ($ webhookMethod );
976+ $ this ->manager ->scheduleTask ($ task );
977+ self ::assertNotNull ($ task ->getId ());
978+ self ::assertEquals (Task::STATUS_SCHEDULED , $ task ->getStatus ());
979+ // clean up so the scheduled task does not interfere with other tests
980+ $ this ->manager ->deleteTask ($ task );
981+ }
982+
983+ public static function localWebhookHostDataProvider (): array {
984+ return [
985+ 'localhost ' => ['http://localhost/hook ' , 'localhost ' ],
986+ 'ipv4 loopback ' => ['http://127.0.0.1:8080/hook ' , '127.0.0.1 ' ],
987+ 'ipv6 loopback ' => ['http://[::1]/hook ' , '[::1] ' ],
988+ 'private network ' => ['https://192.168.1.1/hook ' , '192.168.1.1 ' ],
989+ 'local hostname ' => ['https://server.local/hook ' , 'server.local ' ],
990+ ];
991+ }
992+
993+ #[\PHPUnit \Framework \Attributes \DataProvider('localWebhookHostDataProvider ' )]
994+ public function testProviderShouldBeRegisteredAndLocalWebhookHostFailValidation (string $ webhookUri , string $ host ): void {
995+ $ this ->registrationContext ->expects ($ this ->any ())->method ('getTaskProcessingProviders ' )->willReturn ([
996+ new ServiceRegistration ('test ' , SuccessfulSyncProvider::class)
997+ ]);
998+ $ this ->invalidRemoteHosts = [$ host ];
999+ $ task = new Task (TextToText::ID , ['input ' => 'Hello ' ], 'test ' , null );
1000+ $ task ->setWebhookUri ($ webhookUri );
1001+ $ task ->setWebhookMethod ('HTTP:POST ' );
1002+ self ::expectException (ValidationException::class);
1003+ $ this ->manager ->scheduleTask ($ task );
1004+ }
1005+
1006+ public function testProviderShouldBeRegisteredAndAppApiWebhookSkipsHostValidation (): void {
1007+ $ this ->registrationContext ->expects ($ this ->any ())->method ('getTaskProcessingProviders ' )->willReturn ([
1008+ new ServiceRegistration ('test ' , SuccessfulSyncProvider::class)
1009+ ]);
1010+ // AppAPI webhooks use an absolute path, so no remote host is involved
1011+ $ this ->rejectAllRemoteHosts = true ;
1012+ $ task = new Task (TextToText::ID , ['input ' => 'Hello ' ], 'test ' , null );
1013+ $ task ->setWebhookUri ('/some/path ' );
1014+ $ task ->setWebhookMethod ('AppAPI:my_app:POST ' );
1015+ $ this ->manager ->scheduleTask ($ task );
1016+ self ::assertEquals (Task::STATUS_SCHEDULED , $ task ->getStatus ());
1017+ // clean up so the scheduled task does not interfere with other tests
1018+ $ this ->manager ->deleteTask ($ task );
1019+ }
1020+
9081021 public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation (): void {
9091022 $ this ->registrationContext ->expects ($ this ->any ())->method ('getTaskProcessingTaskTypes ' )->willReturn ([
9101023 new ServiceRegistration ('test ' , AudioToImage::class)
@@ -1597,6 +1710,7 @@ private function createManagerInstance(): Manager {
15971710 Server::get (ICacheFactory::class),
15981711 Server::get (IFactory::class),
15991712 Server::get (ITimeFactory::class),
1713+ $ this ->remoteHostValidator ,
16001714 );
16011715 }
16021716
0 commit comments