WIP
[shpub.git] / bin / shpub.php
1 <?php
2 namespace shpub;
3
4 if (file_exists(__DIR__ . '/../src/shpub/Autoloader.php')) {
5     include_once __DIR__ . '/../src/shpub/Autoloader.php';
6     Autoloader::register();
7 }
8 $cli = new Cli();
9 $cli->run();
10 exit();
11 /**
12  * @link http://micropub.net/draft/
13  * @link http://indieweb.org/authorization-endpoint
14  */
15 $server = 'http://anoweco.bogo/';
16 $user   = 'http://anoweco.bogo/user/3.htm';
17
18 require_once 'HTTP/Request2.php';
19
20 $endpoints = discoverEndpoints($server);
21 list($accessToken, $userUrl) = getAuthCode($user, $endpoints);
22 var_dump($endpoints, $accessToken, $userUrl);
23
24
25 function getAuthCode($user, $endpoints)
26 {
27     //fetch temporary authorization token
28     $redirect_uri = 'http://127.0.0.1:12345/callback';
29     $state        = time();
30     $client_id    = 'http://cweiske.de/shpub.htm';
31
32     $browserUrl = $endpoints->authorization
33         . '?me=' . urlencode($user)
34         . '&client_id=' . urlencode($client_id)
35         . '&redirect_uri=' . urlencode($redirect_uri)
36         . '&state=' . $state
37         . '&scope=post'
38         . '&response_type=code';
39     echo "To authenticate, open the following URL:\n"
40         . $browserUrl . "\n";
41
42     $authParams = startHttpServer();
43
44     if ($authParams['state'] != $state) {
45         logError('Wrong "state" parameter value');
46         exit(2);
47     }
48
49     //verify indieauth params
50     $req = new HTTP_Request2($endpoints->authorization, 'POST');
51     $req->setHeader('Content-Type: application/x-www-form-urlencoded');
52     $req->setBody(
53         http_build_query(
54             [
55                 'code'         => $authParams['code'],
56                 'state'        => $state,
57                 'client_id'    => $client_id,
58                 'redirect_uri' => $redirect_uri,
59             ]
60         )
61     );
62     $res = $req->send();
63     if ($res->getHeader('content-type') != 'application/x-www-form-urlencoded') {
64         logError('Wrong content type in auth verification response');
65         exit(2);
66     }
67     parse_str($res->getBody(), $verifiedParams);
68     if (!isset($verifiedParams['me'])
69         || $verifiedParams['me'] !== $authParams['me']
70     ) {
71         logError('Non-matching "me" values');
72         exit(2);
73     }
74
75     $userUrl = $verifiedParams['me'];
76
77
78     //fetch permanent access token
79     $req = new HTTP_Request2($endpoints->token, 'POST');
80     $req->setHeader('Content-Type: application/x-www-form-urlencoded');
81     $req->setBody(
82         http_build_query(
83             [
84                 'me'           => $userUrl,
85                 'code'         => $authParams['code'],
86                 'redirect_uri' => $redirect_uri,
87                 'client_id'    => $client_id,
88                 'state'        => $state,
89             ]
90         )
91     );
92     $res = $req->send();
93     if ($res->getHeader('content-type') != 'application/x-www-form-urlencoded') {
94         logError('Wrong content type in auth verification response');
95         exit(2);
96     }
97     parse_str($res->getBody(), $tokenParams);
98     if (!isset($tokenParams['access_token'])) {
99         logError('"access_token" missing');
100         exit(2);
101     }
102
103     $accessToken = $tokenParams['access_token'];
104
105     return [$accessToken, $userUrl];
106 }
107
108 function startHttpServer()
109 {
110     $responseOk = "HTTP/1.0 200 OK\r\n"
111         . "Content-Type: text/plain\r\n"
112         . "\r\n"
113         . "Ok. You may close this tab and return to the shell.\r\n";
114     $responseErr = "HTTP/1.0 400 Bad Request\r\n"
115         . "Content-Type: text/plain\r\n"
116         . "\r\n"
117         . "Bad Request\r\n";
118
119     //5 minutes should be enough for the user to confirm
120     ini_set('default_socket_timeout', 60 * 5);
121     $server = stream_socket_server(
122         'tcp://127.0.0.1:12345', $errno, $errstr
123     );
124     if (!$server) {
125         //TODO: log
126         return false;
127     }
128
129     do {
130         $sock = stream_socket_accept($server);
131         if (!$sock) {
132             //TODO: log
133             exit(1);
134         }
135
136         $headers = [];
137         $body    = null;
138         $content_length = 0;
139         //read request headers
140         while (false !== ($line = trim(fgets($sock)))) {
141             if ('' === $line) {
142                 break;
143             }
144             if (preg_match('#^Content-Length:\s*([[:digit:]]+)\s*$#i', $line, $matches)) {
145                 $content_length = (int) $matches[1];
146             }
147             $headers[] = $line;
148         }
149
150         // read content/body
151         if ($content_length > 0) {
152             $body = fread($sock, $content_length);
153         }
154
155         // send response
156         list($method, $url, $httpver) = explode(' ', $headers[0]);
157         if ($method == 'GET') {
158             $parts = parse_url($url);
159             if (isset($parts['path']) && $parts['path'] == '/callback'
160                 && isset($parts['query'])
161             ) {
162                 parse_str($parts['query'], $query);
163                 if (isset($query['code'])
164                     && isset($query['state'])
165                     && isset($query['me'])
166                 ) {
167                     fwrite($sock, $responseOk);
168                     fclose($sock);
169                     return $query;
170                 }
171             }
172         }
173
174         fwrite($sock, $responseErr);
175         fclose($sock);
176     } while(true);
177 }
178
179 class Config_Endpoints
180 {
181     public $micropub;
182     public $media;
183     public $token;
184     public $authorization;
185 }
186
187 function discoverEndpoints($url)
188 {
189     $cfg = new Config_Endpoints();
190
191     //TODO: discovery via link headers
192
193     $sx = simplexml_load_file($url);
194     $sx->registerXPathNamespace('h', 'http://www.w3.org/1999/xhtml');
195
196     $auths = $sx->xpath(
197         '/h:html/h:head/h:link[@rel="authorization_endpoint" and @href]'
198     );
199     if (!count($auths)) {
200         logError('No authorization endpoint found');
201         exit(1);
202     }
203     $cfg->authorization = (string) $auths[0]['href'];
204
205     $tokens = $sx->xpath(
206         '/h:html/h:head/h:link[@rel="token_endpoint" and @href]'
207     );
208     if (!count($tokens)) {
209         logError('No token endpoint found');
210         exit(1);
211     }
212     $cfg->token = (string) $tokens[0]['href'];
213
214     $mps = $sx->xpath(
215         '/h:html/h:head/h:link[@rel="micropub" and @href]'
216     );
217     if (!count($mps)) {
218         logError('No micropub endpoint found');
219         exit(1);
220     }
221     $cfg->micropub = (string) $mps[0]['href'];
222
223     return $cfg;
224 }
225
226 function logError($msg)
227 {
228     file_put_contents('php://stderr', $msg . "\n", FILE_APPEND);
229 }
230 ?>