Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestOAuthConsumer
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 2
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 1
306
1<?php
2
3namespace MediaWiki\Extension\OAuth;
4
5use MediaWiki\Extension\OAuth\Lib\OAuthConsumer;
6use MediaWiki\Extension\OAuth\Lib\OAuthException;
7use MediaWiki\Extension\OAuth\Lib\OAuthRequest;
8use MediaWiki\Extension\OAuth\Lib\OAuthSignatureMethodHmacSha1;
9use MediaWiki\Extension\OAuth\Lib\OAuthSignatureMethodRsaSha1;
10use MediaWiki\MainConfigNames;
11use MediaWiki\Maintenance\Maintenance;
12
13/**
14 * @ingroup Maintenance
15 */
16// @codeCoverageIgnoreStart
17if ( getenv( 'MW_INSTALL_PATH' ) ) {
18    $IP = getenv( 'MW_INSTALL_PATH' );
19} else {
20    $IP = __DIR__ . '/../../..';
21}
22
23require_once "$IP/maintenance/Maintenance.php";
24// @codeCoverageIgnoreEnd
25
26class TestOAuthConsumer extends Maintenance {
27    public function __construct() {
28        parent::__construct();
29        $this->addDescription( "Test an OAuth consumer" );
30        $this->addOption( 'consumerKey', 'Consumer key', true, true );
31        $this->addOption( 'consumerSecret', 'Consumer secret', false, true );
32        $this->addOption( 'RSAKeyFile',
33            'File containing the RSA private key for the consumer', false, true
34        );
35        $this->addOption( 'useSSL', 'Use SSL' );
36        $this->addOption( 'verbose', 'Verbose output (e.g. HTTP request/response headers)' );
37        $this->requireExtension( "OAuth" );
38    }
39
40    public function execute() {
41        $consumerKey = $this->getOption( 'consumerKey' );
42        $consumerSecret = $this->getOption( 'consumerSecret' );
43        $rsaKeyFile = $this->getOption( 'RSAKeyFile' );
44        $config = $this->getServiceContainer()->getMainConfig();
45        $script = $config->get( MainConfigNames::Server ) .
46            $config->get( MainConfigNames::ScriptPath );
47        $baseurl = $this->getServiceContainer()->getUrlUtils()->expand(
48            "$script/index.php?title=Special:OAuth", PROTO_CANONICAL );
49        $endpoint = "$baseurl/initiate&format=json&oauth_callback=oob";
50
51        $endpoint_acc = "$baseurl/token&format=json";
52
53        if ( !$consumerSecret && !$rsaKeyFile ) {
54            $this->error( "Either consumerSecret or RSAKeyFile required!" );
55            $this->maybeHelp( true );
56        }
57
58        $c = new OAuthConsumer( $consumerKey, $consumerSecret );
59        $parsed = parse_url( $endpoint );
60        $params = [];
61        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
62        parse_str( $parsed['query'], $params );
63        $req_req = OAuthRequest::from_consumer_and_token( $c, null, "GET", $endpoint, $params );
64        if ( $rsaKeyFile ) {
65            try {
66                $sig_method = new class ( $rsaKeyFile ) extends OAuthSignatureMethodRsaSha1 {
67                    /** @var string */
68                    private $privKey;
69                    /** @var string */
70                    private $pubKey;
71
72                    public function __construct( string $privKeyFile ) {
73                        $key = file_get_contents( $privKeyFile );
74                        if ( !$key ) {
75                            throw new OAuthException( "Could not read private key file $privKeyFile" );
76                        }
77
78                        $privKey = openssl_pkey_get_private( $key );
79                        if ( !$privKey ) {
80                            throw new OAuthException( "File $privKeyFile does not contain a private key" );
81                        }
82
83                        $details = openssl_pkey_get_details( $privKey );
84                        if ( $details['type'] !== OPENSSL_KEYTYPE_RSA ) {
85                            throw new OAuthException( "Key is not an RSA key" );
86                        }
87                        if ( !$details['key'] ) {
88                            throw new OAuthException( "Could not get public key from private key" );
89                        }
90
91                        $this->privKey = $key;
92                        $this->pubKey = $details['key'];
93                    }
94
95                    /** @inheritDoc */
96                    protected function fetch_public_cert( &$request ) {
97                        return $this->pubKey;
98                    }
99
100                    /** @inheritDoc */
101                    protected function fetch_private_cert( &$request ) {
102                        return $this->privKey;
103                    }
104                };
105            } catch ( OAuthException $ex ) {
106                $this->fatalError( $ex->getMessage() );
107            }
108        } else {
109            $sig_method = new OAuthSignatureMethodHmacSha1();
110        }
111        $req_req->sign_request( $sig_method, $c, null );
112
113        $this->output( "Calling: $req_req\n" );
114
115        $ch = curl_init();
116        curl_setopt( $ch, CURLOPT_URL, (string)$req_req );
117        if ( $this->hasOption( 'useSSL' ) ) {
118            curl_setopt( $ch, CURLOPT_PORT, 443 );
119        }
120        if ( $this->hasOption( 'verbose' ) ) {
121            curl_setopt( $ch, CURLOPT_VERBOSE, true );
122        }
123        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 );
124        curl_setopt( $ch, CURLOPT_HEADER, 0 );
125        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
126        $data = curl_exec( $ch );
127
128        if ( !$data ) {
129            $this->output( 'Curl error: ' . curl_error( $ch ) );
130        }
131
132        $this->output( "Returned: $data\n\n" );
133
134        $token = json_decode( $data );
135        if ( !$token || !isset( $token->key ) ) {
136            $this->fatalError( 'Could not fetch token' );
137        }
138
139        $this->output( "Visit $baseurl/authorize" .
140            "&oauth_token={$token->key}&oauth_consumer_key=$consumerKey\n" );
141
142        // ACCESS TOKEN
143        $this->output( "Enter the verification code:\n" );
144        $fh = fopen( "php://stdin", "r" );
145        $line = fgets( $fh );
146
147        $rc = new OAuthConsumer( $token->key, $token->secret );
148        $parsed = parse_url( $endpoint_acc );
149        // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
150        parse_str( $parsed['query'], $params );
151        $params['oauth_verifier'] = trim( $line );
152
153        $acc_req = OAuthRequest::from_consumer_and_token( $c, $rc, "GET", $endpoint_acc, $params );
154        $acc_req->sign_request( $sig_method, $c, $rc );
155
156        $this->output( "Calling: $acc_req\n" );
157
158        unset( $ch );
159        $ch = curl_init();
160        curl_setopt( $ch, CURLOPT_URL, (string)$acc_req );
161        if ( $this->hasOption( 'useSSL' ) ) {
162            curl_setopt( $ch, CURLOPT_PORT, 443 );
163        }
164        if ( $this->hasOption( 'verbose' ) ) {
165            curl_setopt( $ch, CURLOPT_VERBOSE, true );
166        }
167        curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 );
168        curl_setopt( $ch, CURLOPT_HEADER, 0 );
169        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
170        $data = curl_exec( $ch );
171        if ( !$data ) {
172            $this->output( 'Curl error: ' . curl_error( $ch ) );
173        }
174
175        $this->output( "Returned: $data\n\n" );
176    }
177}
178
179// @codeCoverageIgnoreStart
180$maintClass = TestOAuthConsumer::class;
181require_once RUN_MAINTENANCE_IF_MAIN;
182// @codeCoverageIgnoreEnd