├── play.gsm ├── goodbye.gsm ├── prompt.gsm ├── process.sh ├── tweet.php ├── post.py ├── extensions.conf ├── README.md ├── twitteroauth.php └── OAuth.php /play.gsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ageis/jail2net/HEAD/play.gsm -------------------------------------------------------------------------------- /goodbye.gsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ageis/jail2net/HEAD/goodbye.gsm -------------------------------------------------------------------------------- /prompt.gsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ageis/jail2net/HEAD/prompt.gsm -------------------------------------------------------------------------------- /process.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | data=$1 3 | str="${data%.wav}" 4 | str2="${str##*/}" 5 | str3="$str2.mp3" 6 | str4="${data##*/}" 7 | tmp=/opt/jail2net 8 | 9 | cp $data $tmp 10 | cd $tmp 11 | lame -m m -b 128 $data $str3 12 | python /opt/jail2net/post.py $str3 13 | rm $data 14 | rm $str4 15 | -------------------------------------------------------------------------------- /tweet.php: -------------------------------------------------------------------------------- 1 | post('statuses/update', $update); 12 | 13 | ?> 14 | -------------------------------------------------------------------------------- /post.py: -------------------------------------------------------------------------------- 1 | import soundcloud 2 | import sys 3 | import datetime 4 | import subprocess 5 | 6 | client = soundcloud.Client(client_id='', 7 | client_secret='', 8 | username='', 9 | password='') 10 | 11 | date = str(datetime.date.today()) 12 | 13 | track = client.post('/tracks', track={ 14 | 'title': '____ - Live From Prison - ' + date, 15 | 'sharing': 'public', 16 | 'asset_data': open(sys.argv[1], 'rb') 17 | }) 18 | 19 | subprocess.call(["php5", "tweet.php", track.permalink_url]) 20 | -------------------------------------------------------------------------------- /extensions.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | ; fill in with your DID 3 | exten => 5555555555,1,Answer() 4 | exten => 5555555555,n,System(/bin/echo "${STRFTIME(${EPOCH})} ${CALLERID(number)} ${CALLERID(name)}" >> /opt/jail2net/callerid.log) 5 | exten => 5555555555,n,Goto(main,s,1) 6 | 7 | [main] 8 | exten => s,1,Wait(10) 9 | ; uncomment and modify to explicitly allow or deny numbers 10 | ;exten => s,n,GotoIf($["${CALLERID(num)}" = "8005555555"]?accept,s,1) 11 | ;exten => s,n,GotoIf($["${CALLERID(num)}" = "2023243000"]?deny,s,1) 12 | exten => s,n,Goto(accept,s,1) 13 | 14 | [accept] 15 | ; some prison phone systems may require a DTMF tone of 1 to accept a call instead 16 | exten => s,1,SendDTMF(5) 17 | exten => s,n,Wait(3) 18 | exten => s,n,Playback(prompt) 19 | exten => s,n,Record(msg%d:wav,,,k) 20 | exten => s,n,Playback(play) 21 | exten => s,n,Playback(${RECORDED_FILE}) 22 | exten => s,n,Playback(goodbye) 23 | exten => h,1,System(/opt/jail2net/process.sh /usr/share/asterisk/sounds/${RECORDED_FILE}.wav) 24 | 25 | [deny] 26 | exten => i,1,Hangup() 27 | exten => t,1,Hangup() 28 | exten => s,1,Hangup() 29 | exten => h,1,Hangup() 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jail2net 2 | ========= 3 | Allows creation of a PBX to accept calls from prisoners and post their recorded messages on SoundCloud and Twitter. Makes the Bureau of Prisons really mad. 4 | 5 | The idea is to set up a phone message box for prisoners that automatically sends the correct tone to accept their call, records a WAV which gets converted to an MP3, and uploads it to the Internet. 6 | 7 | ###Requirements: 8 | 9 | Asterisk, lame, Python, PHP, Bash, soundcloud-python, twitteroauth, a DID number and SIP provider 10 | 11 | ###Instructions: 12 | 13 | 1. Purchase a DID (telephone number) from a SIP provider. I recommend [VoIP.ms](https://voip.ms) 14 | 2. Install Asterisk and [configure your sip.conf](http://wiki.voip.ms/article/PBXs#Asterisk_.28SIP.29). Set context= to default. 15 | 3. Clone this repository to /opt/jail2net 16 | 4. `chown -R asterisk:asterisk /opt/jail2net` 17 | 5. `apt-get install lame` and `pip install soundcloud` 18 | 6. Move *.gsm files to /usr/share/asterisk/sounds 19 | 7. Make process.sh, post.py and tweet.php executable using chmod +x 20 | 8. Edit post.py and insert your [SoundCloud API details](http://soundcloud.com/settings/connections) for client_id, client_secret, username, password 21 | 9. Change the track title as desired 22 | 10. Edit tweet.php and insert your [Twitter API details](https://dev.twitter.com/) for $consumerKey, $consumerSecret, $oAuthToken and $oAuthSecret 23 | 11. Change the status message as desired 24 | 12. Move extensions.conf to /etc/asterisk, edit and insert your DID, set up access control and DTMF tone if needed 25 | 13. Restart Asterisk and give the phone number to a prisoner. Have fun trolling the BOP! 26 | 27 | It helps to know what number they'll be calling from and the digit you need to dial to accept their call. 28 | 29 | This concept has been [covered by Mashable](http://mashable.com/2013/04/15/weev-soundcloud-message/). Mad props to Jaime Cochran for the idea and .py SoundCloud script. Prisoners should be informed of the risks; weev lost his phone privileges and was thrown in solitary for using this. 30 | -------------------------------------------------------------------------------- /twitteroauth.php: -------------------------------------------------------------------------------- 1 | http_status; } 54 | function lastAPICall() { return $this->last_api_call; } 55 | 56 | /** 57 | * construct TwitterOAuth object 58 | */ 59 | function __construct($consumer_key, $consumer_secret, $oauth_token = NULL, $oauth_token_secret = NULL) { 60 | $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1(); 61 | $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret); 62 | if (!empty($oauth_token) && !empty($oauth_token_secret)) { 63 | $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret); 64 | } else { 65 | $this->token = NULL; 66 | } 67 | } 68 | 69 | 70 | /** 71 | * Get a request_token from Twitter 72 | * 73 | * @returns a key/value array containing oauth_token and oauth_token_secret 74 | */ 75 | function getRequestToken($oauth_callback) { 76 | $parameters = array(); 77 | $parameters['oauth_callback'] = $oauth_callback; 78 | $request = $this->oAuthRequest($this->requestTokenURL(), 'GET', $parameters); 79 | $token = OAuthUtil::parse_parameters($request); 80 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 81 | return $token; 82 | } 83 | 84 | /** 85 | * Get the authorize URL 86 | * 87 | * @returns a string 88 | */ 89 | function getAuthorizeURL($token, $sign_in_with_twitter = TRUE) { 90 | if (is_array($token)) { 91 | $token = $token['oauth_token']; 92 | } 93 | if (empty($sign_in_with_twitter)) { 94 | return $this->authorizeURL() . "?oauth_token={$token}"; 95 | } else { 96 | return $this->authenticateURL() . "?oauth_token={$token}"; 97 | } 98 | } 99 | 100 | /** 101 | * Exchange request token and secret for an access token and 102 | * secret, to sign API calls. 103 | * 104 | * @returns array("oauth_token" => "the-access-token", 105 | * "oauth_token_secret" => "the-access-secret", 106 | * "user_id" => "9436992", 107 | * "screen_name" => "abraham") 108 | */ 109 | function getAccessToken($oauth_verifier) { 110 | $parameters = array(); 111 | $parameters['oauth_verifier'] = $oauth_verifier; 112 | $request = $this->oAuthRequest($this->accessTokenURL(), 'GET', $parameters); 113 | $token = OAuthUtil::parse_parameters($request); 114 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 115 | return $token; 116 | } 117 | 118 | /** 119 | * One time exchange of username and password for access token and secret. 120 | * 121 | * @returns array("oauth_token" => "the-access-token", 122 | * "oauth_token_secret" => "the-access-secret", 123 | * "user_id" => "9436992", 124 | * "screen_name" => "abraham", 125 | * "x_auth_expires" => "0") 126 | */ 127 | function getXAuthToken($username, $password) { 128 | $parameters = array(); 129 | $parameters['x_auth_username'] = $username; 130 | $parameters['x_auth_password'] = $password; 131 | $parameters['x_auth_mode'] = 'client_auth'; 132 | $request = $this->oAuthRequest($this->accessTokenURL(), 'POST', $parameters); 133 | $token = OAuthUtil::parse_parameters($request); 134 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']); 135 | return $token; 136 | } 137 | 138 | /** 139 | * GET wrapper for oAuthRequest. 140 | */ 141 | function get($url, $parameters = array()) { 142 | $response = $this->oAuthRequest($url, 'GET', $parameters); 143 | if ($this->format === 'json' && $this->decode_json) { 144 | return json_decode($response); 145 | } 146 | return $response; 147 | } 148 | 149 | /** 150 | * POST wrapper for oAuthRequest. 151 | */ 152 | function post($url, $parameters = array()) { 153 | $response = $this->oAuthRequest($url, 'POST', $parameters); 154 | if ($this->format === 'json' && $this->decode_json) { 155 | return json_decode($response); 156 | } 157 | return $response; 158 | } 159 | 160 | /** 161 | * DELETE wrapper for oAuthReqeust. 162 | */ 163 | function delete($url, $parameters = array()) { 164 | $response = $this->oAuthRequest($url, 'DELETE', $parameters); 165 | if ($this->format === 'json' && $this->decode_json) { 166 | return json_decode($response); 167 | } 168 | return $response; 169 | } 170 | 171 | /** 172 | * Format and sign an OAuth / API request 173 | */ 174 | function oAuthRequest($url, $method, $parameters) { 175 | if (strrpos($url, 'https://') !== 0 && strrpos($url, 'http://') !== 0) { 176 | $url = "{$this->host}{$url}.{$this->format}"; 177 | } 178 | $request = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $parameters); 179 | $request->sign_request($this->sha1_method, $this->consumer, $this->token); 180 | switch ($method) { 181 | case 'GET': 182 | return $this->http($request->to_url(), 'GET'); 183 | default: 184 | return $this->http($request->get_normalized_http_url(), $method, $request->to_postdata()); 185 | } 186 | } 187 | 188 | /** 189 | * Make an HTTP request 190 | * 191 | * @return API results 192 | */ 193 | function http($url, $method, $postfields = NULL) { 194 | $this->http_info = array(); 195 | $ci = curl_init(); 196 | /* Curl settings */ 197 | curl_setopt($ci, CURLOPT_USERAGENT, $this->useragent); 198 | curl_setopt($ci, CURLOPT_CONNECTTIMEOUT, $this->connecttimeout); 199 | curl_setopt($ci, CURLOPT_TIMEOUT, $this->timeout); 200 | curl_setopt($ci, CURLOPT_RETURNTRANSFER, TRUE); 201 | curl_setopt($ci, CURLOPT_HTTPHEADER, array('Expect:')); 202 | curl_setopt($ci, CURLOPT_SSL_VERIFYPEER, $this->ssl_verifypeer); 203 | curl_setopt($ci, CURLOPT_HEADERFUNCTION, array($this, 'getHeader')); 204 | curl_setopt($ci, CURLOPT_HEADER, FALSE); 205 | 206 | switch ($method) { 207 | case 'POST': 208 | curl_setopt($ci, CURLOPT_POST, TRUE); 209 | if (!empty($postfields)) { 210 | curl_setopt($ci, CURLOPT_POSTFIELDS, $postfields); 211 | } 212 | break; 213 | case 'DELETE': 214 | curl_setopt($ci, CURLOPT_CUSTOMREQUEST, 'DELETE'); 215 | if (!empty($postfields)) { 216 | $url = "{$url}?{$postfields}"; 217 | } 218 | } 219 | 220 | curl_setopt($ci, CURLOPT_URL, $url); 221 | $response = curl_exec($ci); 222 | $this->http_code = curl_getinfo($ci, CURLINFO_HTTP_CODE); 223 | $this->http_info = array_merge($this->http_info, curl_getinfo($ci)); 224 | $this->url = $url; 225 | curl_close ($ci); 226 | return $response; 227 | } 228 | 229 | /** 230 | * Get the header info to store. 231 | */ 232 | function getHeader($ch, $header) { 233 | $i = strpos($header, ':'); 234 | if (!empty($i)) { 235 | $key = str_replace('-', '_', strtolower(substr($header, 0, $i))); 236 | $value = trim(substr($header, $i + 2)); 237 | $this->http_header[$key] = $value; 238 | } 239 | return strlen($header); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /OAuth.php: -------------------------------------------------------------------------------- 1 | key = $key; 16 | $this->secret = $secret; 17 | $this->callback_url = $callback_url; 18 | } 19 | 20 | function __toString() { 21 | return "OAuthConsumer[key=$this->key,secret=$this->secret]"; 22 | } 23 | } 24 | 25 | class OAuthToken { 26 | // access tokens and request tokens 27 | public $key; 28 | public $secret; 29 | 30 | /** 31 | * key = the token 32 | * secret = the token secret 33 | */ 34 | function __construct($key, $secret) { 35 | $this->key = $key; 36 | $this->secret = $secret; 37 | } 38 | 39 | /** 40 | * generates the basic string serialization of a token that a server 41 | * would respond to request_token and access_token calls with 42 | */ 43 | function to_string() { 44 | return "oauth_token=" . 45 | OAuthUtil::urlencode_rfc3986($this->key) . 46 | "&oauth_token_secret=" . 47 | OAuthUtil::urlencode_rfc3986($this->secret); 48 | } 49 | 50 | function __toString() { 51 | return $this->to_string(); 52 | } 53 | } 54 | 55 | /** 56 | * A class for implementing a Signature Method 57 | * See section 9 ("Signing Requests") in the spec 58 | */ 59 | abstract class OAuthSignatureMethod { 60 | /** 61 | * Needs to return the name of the Signature Method (ie HMAC-SHA1) 62 | * @return string 63 | */ 64 | abstract public function get_name(); 65 | 66 | /** 67 | * Build up the signature 68 | * NOTE: The output of this function MUST NOT be urlencoded. 69 | * the encoding is handled in OAuthRequest when the final 70 | * request is serialized 71 | * @param OAuthRequest $request 72 | * @param OAuthConsumer $consumer 73 | * @param OAuthToken $token 74 | * @return string 75 | */ 76 | abstract public function build_signature($request, $consumer, $token); 77 | 78 | /** 79 | * Verifies that a given signature is correct 80 | * @param OAuthRequest $request 81 | * @param OAuthConsumer $consumer 82 | * @param OAuthToken $token 83 | * @param string $signature 84 | * @return bool 85 | */ 86 | public function check_signature($request, $consumer, $token, $signature) { 87 | $built = $this->build_signature($request, $consumer, $token); 88 | 89 | // Check for zero length, although unlikely here 90 | if (strlen($built) == 0 || strlen($signature) == 0) { 91 | return false; 92 | } 93 | 94 | if (strlen($built) != strlen($signature)) { 95 | return false; 96 | } 97 | 98 | // Avoid a timing leak with a (hopefully) time insensitive compare 99 | $result = 0; 100 | for ($i = 0; $i < strlen($signature); $i++) { 101 | $result |= ord($built{$i}) ^ ord($signature{$i}); 102 | } 103 | 104 | return $result == 0; 105 | } 106 | } 107 | 108 | /** 109 | * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] 110 | * where the Signature Base String is the text and the key is the concatenated values (each first 111 | * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' 112 | * character (ASCII code 38) even if empty. 113 | * - Chapter 9.2 ("HMAC-SHA1") 114 | */ 115 | class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { 116 | function get_name() { 117 | return "HMAC-SHA1"; 118 | } 119 | 120 | public function build_signature($request, $consumer, $token) { 121 | $base_string = $request->get_signature_base_string(); 122 | $request->base_string = $base_string; 123 | 124 | $key_parts = array( 125 | $consumer->secret, 126 | ($token) ? $token->secret : "" 127 | ); 128 | 129 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 130 | $key = implode('&', $key_parts); 131 | 132 | return base64_encode(hash_hmac('sha1', $base_string, $key, true)); 133 | } 134 | } 135 | 136 | /** 137 | * The PLAINTEXT method does not provide any security protection and SHOULD only be used 138 | * over a secure channel such as HTTPS. It does not use the Signature Base String. 139 | * - Chapter 9.4 ("PLAINTEXT") 140 | */ 141 | class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { 142 | public function get_name() { 143 | return "PLAINTEXT"; 144 | } 145 | 146 | /** 147 | * oauth_signature is set to the concatenated encoded values of the Consumer Secret and 148 | * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is 149 | * empty. The result MUST be encoded again. 150 | * - Chapter 9.4.1 ("Generating Signatures") 151 | * 152 | * Please note that the second encoding MUST NOT happen in the SignatureMethod, as 153 | * OAuthRequest handles this! 154 | */ 155 | public function build_signature($request, $consumer, $token) { 156 | $key_parts = array( 157 | $consumer->secret, 158 | ($token) ? $token->secret : "" 159 | ); 160 | 161 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 162 | $key = implode('&', $key_parts); 163 | $request->base_string = $key; 164 | 165 | return $key; 166 | } 167 | } 168 | 169 | /** 170 | * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in 171 | * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for 172 | * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a 173 | * verified way to the Service Provider, in a manner which is beyond the scope of this 174 | * specification. 175 | * - Chapter 9.3 ("RSA-SHA1") 176 | */ 177 | abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { 178 | public function get_name() { 179 | return "RSA-SHA1"; 180 | } 181 | 182 | // Up to the SP to implement this lookup of keys. Possible ideas are: 183 | // (1) do a lookup in a table of trusted certs keyed off of consumer 184 | // (2) fetch via http using a url provided by the requester 185 | // (3) some sort of specific discovery code based on request 186 | // 187 | // Either way should return a string representation of the certificate 188 | protected abstract function fetch_public_cert(&$request); 189 | 190 | // Up to the SP to implement this lookup of keys. Possible ideas are: 191 | // (1) do a lookup in a table of trusted certs keyed off of consumer 192 | // 193 | // Either way should return a string representation of the certificate 194 | protected abstract function fetch_private_cert(&$request); 195 | 196 | public function build_signature($request, $consumer, $token) { 197 | $base_string = $request->get_signature_base_string(); 198 | $request->base_string = $base_string; 199 | 200 | // Fetch the private key cert based on the request 201 | $cert = $this->fetch_private_cert($request); 202 | 203 | // Pull the private key ID from the certificate 204 | $privatekeyid = openssl_get_privatekey($cert); 205 | 206 | // Sign using the key 207 | $ok = openssl_sign($base_string, $signature, $privatekeyid); 208 | 209 | // Release the key resource 210 | openssl_free_key($privatekeyid); 211 | 212 | return base64_encode($signature); 213 | } 214 | 215 | public function check_signature($request, $consumer, $token, $signature) { 216 | $decoded_sig = base64_decode($signature); 217 | 218 | $base_string = $request->get_signature_base_string(); 219 | 220 | // Fetch the public key cert based on the request 221 | $cert = $this->fetch_public_cert($request); 222 | 223 | // Pull the public key ID from the certificate 224 | $publickeyid = openssl_get_publickey($cert); 225 | 226 | // Check the computed signature against the one passed in the query 227 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); 228 | 229 | // Release the key resource 230 | openssl_free_key($publickeyid); 231 | 232 | return $ok == 1; 233 | } 234 | } 235 | 236 | class OAuthRequest { 237 | protected $parameters; 238 | protected $http_method; 239 | protected $http_url; 240 | // for debug purposes 241 | public $base_string; 242 | public static $version = '1.0'; 243 | public static $POST_INPUT = 'php://input'; 244 | 245 | function __construct($http_method, $http_url, $parameters=NULL) { 246 | $parameters = ($parameters) ? $parameters : array(); 247 | $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters); 248 | $this->parameters = $parameters; 249 | $this->http_method = $http_method; 250 | $this->http_url = $http_url; 251 | } 252 | 253 | 254 | /** 255 | * attempt to build up a request from what was passed to the server 256 | */ 257 | public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { 258 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") 259 | ? 'http' 260 | : 'https'; 261 | $http_url = ($http_url) ? $http_url : $scheme . 262 | '://' . $_SERVER['SERVER_NAME'] . 263 | ':' . 264 | $_SERVER['SERVER_PORT'] . 265 | $_SERVER['REQUEST_URI']; 266 | $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD']; 267 | 268 | // We weren't handed any parameters, so let's find the ones relevant to 269 | // this request. 270 | // If you run XML-RPC or similar you should use this to provide your own 271 | // parsed parameter-list 272 | if (!$parameters) { 273 | // Find request headers 274 | $request_headers = OAuthUtil::get_headers(); 275 | 276 | // Parse the query-string to find GET parameters 277 | $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); 278 | 279 | // It's a POST request of the proper content-type, so parse POST 280 | // parameters and add those overriding any duplicates from GET 281 | if ($http_method == "POST" 282 | && isset($request_headers['Content-Type']) 283 | && strstr($request_headers['Content-Type'], 284 | 'application/x-www-form-urlencoded') 285 | ) { 286 | $post_data = OAuthUtil::parse_parameters( 287 | file_get_contents(self::$POST_INPUT) 288 | ); 289 | $parameters = array_merge($parameters, $post_data); 290 | } 291 | 292 | // We have a Authorization-header with OAuth data. Parse the header 293 | // and add those overriding any duplicates from GET or POST 294 | if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') { 295 | $header_parameters = OAuthUtil::split_header( 296 | $request_headers['Authorization'] 297 | ); 298 | $parameters = array_merge($parameters, $header_parameters); 299 | } 300 | 301 | } 302 | 303 | return new OAuthRequest($http_method, $http_url, $parameters); 304 | } 305 | 306 | /** 307 | * pretty much a helper function to set up the request 308 | */ 309 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { 310 | $parameters = ($parameters) ? $parameters : array(); 311 | $defaults = array("oauth_version" => OAuthRequest::$version, 312 | "oauth_nonce" => OAuthRequest::generate_nonce(), 313 | "oauth_timestamp" => OAuthRequest::generate_timestamp(), 314 | "oauth_consumer_key" => $consumer->key); 315 | if ($token) 316 | $defaults['oauth_token'] = $token->key; 317 | 318 | $parameters = array_merge($defaults, $parameters); 319 | 320 | return new OAuthRequest($http_method, $http_url, $parameters); 321 | } 322 | 323 | public function set_parameter($name, $value, $allow_duplicates = true) { 324 | if ($allow_duplicates && isset($this->parameters[$name])) { 325 | // We have already added parameter(s) with this name, so add to the list 326 | if (is_scalar($this->parameters[$name])) { 327 | // This is the first duplicate, so transform scalar (string) 328 | // into an array so we can add the duplicates 329 | $this->parameters[$name] = array($this->parameters[$name]); 330 | } 331 | 332 | $this->parameters[$name][] = $value; 333 | } else { 334 | $this->parameters[$name] = $value; 335 | } 336 | } 337 | 338 | public function get_parameter($name) { 339 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 340 | } 341 | 342 | public function get_parameters() { 343 | return $this->parameters; 344 | } 345 | 346 | public function unset_parameter($name) { 347 | unset($this->parameters[$name]); 348 | } 349 | 350 | /** 351 | * The request parameters, sorted and concatenated into a normalized string. 352 | * @return string 353 | */ 354 | public function get_signable_parameters() { 355 | // Grab all parameters 356 | $params = $this->parameters; 357 | 358 | // Remove oauth_signature if present 359 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 360 | if (isset($params['oauth_signature'])) { 361 | unset($params['oauth_signature']); 362 | } 363 | 364 | return OAuthUtil::build_http_query($params); 365 | } 366 | 367 | /** 368 | * Returns the base string of this request 369 | * 370 | * The base string defined as the method, the url 371 | * and the parameters (normalized), each urlencoded 372 | * and the concated with &. 373 | */ 374 | public function get_signature_base_string() { 375 | $parts = array( 376 | $this->get_normalized_http_method(), 377 | $this->get_normalized_http_url(), 378 | $this->get_signable_parameters() 379 | ); 380 | 381 | $parts = OAuthUtil::urlencode_rfc3986($parts); 382 | 383 | return implode('&', $parts); 384 | } 385 | 386 | /** 387 | * just uppercases the http method 388 | */ 389 | public function get_normalized_http_method() { 390 | return strtoupper($this->http_method); 391 | } 392 | 393 | /** 394 | * parses the url and rebuilds it to be 395 | * scheme://host/path 396 | */ 397 | public function get_normalized_http_url() { 398 | $parts = parse_url($this->http_url); 399 | 400 | $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http'; 401 | $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80'); 402 | $host = (isset($parts['host'])) ? strtolower($parts['host']) : ''; 403 | $path = (isset($parts['path'])) ? $parts['path'] : ''; 404 | 405 | if (($scheme == 'https' && $port != '443') 406 | || ($scheme == 'http' && $port != '80')) { 407 | $host = "$host:$port"; 408 | } 409 | return "$scheme://$host$path"; 410 | } 411 | 412 | /** 413 | * builds a url usable for a GET request 414 | */ 415 | public function to_url() { 416 | $post_data = $this->to_postdata(); 417 | $out = $this->get_normalized_http_url(); 418 | if ($post_data) { 419 | $out .= '?'.$post_data; 420 | } 421 | return $out; 422 | } 423 | 424 | /** 425 | * builds the data one would send in a POST request 426 | */ 427 | public function to_postdata() { 428 | return OAuthUtil::build_http_query($this->parameters); 429 | } 430 | 431 | /** 432 | * builds the Authorization: header 433 | */ 434 | public function to_header($realm=null) { 435 | $first = true; 436 | if($realm) { 437 | $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; 438 | $first = false; 439 | } else 440 | $out = 'Authorization: OAuth'; 441 | 442 | $total = array(); 443 | foreach ($this->parameters as $k => $v) { 444 | if (substr($k, 0, 5) != "oauth") continue; 445 | if (is_array($v)) { 446 | throw new OAuthException('Arrays not supported in headers'); 447 | } 448 | $out .= ($first) ? ' ' : ','; 449 | $out .= OAuthUtil::urlencode_rfc3986($k) . 450 | '="' . 451 | OAuthUtil::urlencode_rfc3986($v) . 452 | '"'; 453 | $first = false; 454 | } 455 | return $out; 456 | } 457 | 458 | public function __toString() { 459 | return $this->to_url(); 460 | } 461 | 462 | 463 | public function sign_request($signature_method, $consumer, $token) { 464 | $this->set_parameter( 465 | "oauth_signature_method", 466 | $signature_method->get_name(), 467 | false 468 | ); 469 | $signature = $this->build_signature($signature_method, $consumer, $token); 470 | $this->set_parameter("oauth_signature", $signature, false); 471 | } 472 | 473 | public function build_signature($signature_method, $consumer, $token) { 474 | $signature = $signature_method->build_signature($this, $consumer, $token); 475 | return $signature; 476 | } 477 | 478 | /** 479 | * util function: current timestamp 480 | */ 481 | private static function generate_timestamp() { 482 | return time(); 483 | } 484 | 485 | /** 486 | * util function: current nonce 487 | */ 488 | private static function generate_nonce() { 489 | $mt = microtime(); 490 | $rand = mt_rand(); 491 | 492 | return md5($mt . $rand); // md5s look nicer than numbers 493 | } 494 | } 495 | 496 | class OAuthServer { 497 | protected $timestamp_threshold = 300; // in seconds, five minutes 498 | protected $version = '1.0'; // hi blaine 499 | protected $signature_methods = array(); 500 | 501 | protected $data_store; 502 | 503 | function __construct($data_store) { 504 | $this->data_store = $data_store; 505 | } 506 | 507 | public function add_signature_method($signature_method) { 508 | $this->signature_methods[$signature_method->get_name()] = 509 | $signature_method; 510 | } 511 | 512 | // high level functions 513 | 514 | /** 515 | * process a request_token request 516 | * returns the request token on success 517 | */ 518 | public function fetch_request_token(&$request) { 519 | $this->get_version($request); 520 | 521 | $consumer = $this->get_consumer($request); 522 | 523 | // no token required for the initial token request 524 | $token = NULL; 525 | 526 | $this->check_signature($request, $consumer, $token); 527 | 528 | // Rev A change 529 | $callback = $request->get_parameter('oauth_callback'); 530 | $new_token = $this->data_store->new_request_token($consumer, $callback); 531 | 532 | return $new_token; 533 | } 534 | 535 | /** 536 | * process an access_token request 537 | * returns the access token on success 538 | */ 539 | public function fetch_access_token(&$request) { 540 | $this->get_version($request); 541 | 542 | $consumer = $this->get_consumer($request); 543 | 544 | // requires authorized request token 545 | $token = $this->get_token($request, $consumer, "request"); 546 | 547 | $this->check_signature($request, $consumer, $token); 548 | 549 | // Rev A change 550 | $verifier = $request->get_parameter('oauth_verifier'); 551 | $new_token = $this->data_store->new_access_token($token, $consumer, $verifier); 552 | 553 | return $new_token; 554 | } 555 | 556 | /** 557 | * verify an api call, checks all the parameters 558 | */ 559 | public function verify_request(&$request) { 560 | $this->get_version($request); 561 | $consumer = $this->get_consumer($request); 562 | $token = $this->get_token($request, $consumer, "access"); 563 | $this->check_signature($request, $consumer, $token); 564 | return array($consumer, $token); 565 | } 566 | 567 | // Internals from here 568 | /** 569 | * version 1 570 | */ 571 | private function get_version(&$request) { 572 | $version = $request->get_parameter("oauth_version"); 573 | if (!$version) { 574 | // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present. 575 | // Chapter 7.0 ("Accessing Protected Ressources") 576 | $version = '1.0'; 577 | } 578 | if ($version !== $this->version) { 579 | throw new OAuthException("OAuth version '$version' not supported"); 580 | } 581 | return $version; 582 | } 583 | 584 | /** 585 | * figure out the signature with some defaults 586 | */ 587 | private function get_signature_method($request) { 588 | $signature_method = $request instanceof OAuthRequest 589 | ? $request->get_parameter("oauth_signature_method") 590 | : NULL; 591 | 592 | if (!$signature_method) { 593 | // According to chapter 7 ("Accessing Protected Ressources") the signature-method 594 | // parameter is required, and we can't just fallback to PLAINTEXT 595 | throw new OAuthException('No signature method parameter. This parameter is required'); 596 | } 597 | 598 | if (!in_array($signature_method, 599 | array_keys($this->signature_methods))) { 600 | throw new OAuthException( 601 | "Signature method '$signature_method' not supported " . 602 | "try one of the following: " . 603 | implode(", ", array_keys($this->signature_methods)) 604 | ); 605 | } 606 | return $this->signature_methods[$signature_method]; 607 | } 608 | 609 | /** 610 | * try to find the consumer for the provided request's consumer key 611 | */ 612 | private function get_consumer($request) { 613 | $consumer_key = $request instanceof OAuthRequest 614 | ? $request->get_parameter("oauth_consumer_key") 615 | : NULL; 616 | 617 | if (!$consumer_key) { 618 | throw new OAuthException("Invalid consumer key"); 619 | } 620 | 621 | $consumer = $this->data_store->lookup_consumer($consumer_key); 622 | if (!$consumer) { 623 | throw new OAuthException("Invalid consumer"); 624 | } 625 | 626 | return $consumer; 627 | } 628 | 629 | /** 630 | * try to find the token for the provided request's token key 631 | */ 632 | private function get_token($request, $consumer, $token_type="access") { 633 | $token_field = $request instanceof OAuthRequest 634 | ? $request->get_parameter('oauth_token') 635 | : NULL; 636 | 637 | $token = $this->data_store->lookup_token( 638 | $consumer, $token_type, $token_field 639 | ); 640 | if (!$token) { 641 | throw new OAuthException("Invalid $token_type token: $token_field"); 642 | } 643 | return $token; 644 | } 645 | 646 | /** 647 | * all-in-one function to check the signature on a request 648 | * should guess the signature method appropriately 649 | */ 650 | private function check_signature($request, $consumer, $token) { 651 | // this should probably be in a different method 652 | $timestamp = $request instanceof OAuthRequest 653 | ? $request->get_parameter('oauth_timestamp') 654 | : NULL; 655 | $nonce = $request instanceof OAuthRequest 656 | ? $request->get_parameter('oauth_nonce') 657 | : NULL; 658 | 659 | $this->check_timestamp($timestamp); 660 | $this->check_nonce($consumer, $token, $nonce, $timestamp); 661 | 662 | $signature_method = $this->get_signature_method($request); 663 | 664 | $signature = $request->get_parameter('oauth_signature'); 665 | $valid_sig = $signature_method->check_signature( 666 | $request, 667 | $consumer, 668 | $token, 669 | $signature 670 | ); 671 | 672 | if (!$valid_sig) { 673 | throw new OAuthException("Invalid signature"); 674 | } 675 | } 676 | 677 | /** 678 | * check that the timestamp is new enough 679 | */ 680 | private function check_timestamp($timestamp) { 681 | if( ! $timestamp ) 682 | throw new OAuthException( 683 | 'Missing timestamp parameter. The parameter is required' 684 | ); 685 | 686 | // verify that timestamp is recentish 687 | $now = time(); 688 | if (abs($now - $timestamp) > $this->timestamp_threshold) { 689 | throw new OAuthException( 690 | "Expired timestamp, yours $timestamp, ours $now" 691 | ); 692 | } 693 | } 694 | 695 | /** 696 | * check that the nonce is not repeated 697 | */ 698 | private function check_nonce($consumer, $token, $nonce, $timestamp) { 699 | if( ! $nonce ) 700 | throw new OAuthException( 701 | 'Missing nonce parameter. The parameter is required' 702 | ); 703 | 704 | // verify that the nonce is uniqueish 705 | $found = $this->data_store->lookup_nonce( 706 | $consumer, 707 | $token, 708 | $nonce, 709 | $timestamp 710 | ); 711 | if ($found) { 712 | throw new OAuthException("Nonce already used: $nonce"); 713 | } 714 | } 715 | 716 | } 717 | 718 | class OAuthDataStore { 719 | function lookup_consumer($consumer_key) { 720 | // implement me 721 | } 722 | 723 | function lookup_token($consumer, $token_type, $token) { 724 | // implement me 725 | } 726 | 727 | function lookup_nonce($consumer, $token, $nonce, $timestamp) { 728 | // implement me 729 | } 730 | 731 | function new_request_token($consumer, $callback = null) { 732 | // return a new token attached to this consumer 733 | } 734 | 735 | function new_access_token($token, $consumer, $verifier = null) { 736 | // return a new access token attached to this consumer 737 | // for the user associated with this token if the request token 738 | // is authorized 739 | // should also invalidate the request token 740 | } 741 | 742 | } 743 | 744 | class OAuthUtil { 745 | public static function urlencode_rfc3986($input) { 746 | if (is_array($input)) { 747 | return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); 748 | } else if (is_scalar($input)) { 749 | return str_replace( 750 | '+', 751 | ' ', 752 | str_replace('%7E', '~', rawurlencode($input)) 753 | ); 754 | } else { 755 | return ''; 756 | } 757 | } 758 | 759 | 760 | // This decode function isn't taking into consideration the above 761 | // modifications to the encoding process. However, this method doesn't 762 | // seem to be used anywhere so leaving it as is. 763 | public static function urldecode_rfc3986($string) { 764 | return urldecode($string); 765 | } 766 | 767 | // Utility function for turning the Authorization: header into 768 | // parameters, has to do some unescaping 769 | // Can filter out any non-oauth parameters if needed (default behaviour) 770 | // May 28th, 2010 - method updated to tjerk.meesters for a speed improvement. 771 | // see http://code.google.com/p/oauth/issues/detail?id=163 772 | public static function split_header($header, $only_allow_oauth_parameters = true) { 773 | $params = array(); 774 | if (preg_match_all('/('.($only_allow_oauth_parameters ? 'oauth_' : '').'[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches)) { 775 | foreach ($matches[1] as $i => $h) { 776 | $params[$h] = OAuthUtil::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]); 777 | } 778 | if (isset($params['realm'])) { 779 | unset($params['realm']); 780 | } 781 | } 782 | return $params; 783 | } 784 | 785 | // helper to try to sort out headers for people who aren't running apache 786 | public static function get_headers() { 787 | if (function_exists('apache_request_headers')) { 788 | // we need this to get the actual Authorization: header 789 | // because apache tends to tell us it doesn't exist 790 | $headers = apache_request_headers(); 791 | 792 | // sanitize the output of apache_request_headers because 793 | // we always want the keys to be Cased-Like-This and arh() 794 | // returns the headers in the same case as they are in the 795 | // request 796 | $out = array(); 797 | foreach ($headers AS $key => $value) { 798 | $key = str_replace( 799 | " ", 800 | "-", 801 | ucwords(strtolower(str_replace("-", " ", $key))) 802 | ); 803 | $out[$key] = $value; 804 | } 805 | } else { 806 | // otherwise we don't have apache and are just going to have to hope 807 | // that $_SERVER actually contains what we need 808 | $out = array(); 809 | if( isset($_SERVER['CONTENT_TYPE']) ) 810 | $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; 811 | if( isset($_ENV['CONTENT_TYPE']) ) 812 | $out['Content-Type'] = $_ENV['CONTENT_TYPE']; 813 | 814 | foreach ($_SERVER as $key => $value) { 815 | if (substr($key, 0, 5) == "HTTP_") { 816 | // this is chaos, basically it is just there to capitalize the first 817 | // letter of every word that is not an initial HTTP and strip HTTP 818 | // code from przemek 819 | $key = str_replace( 820 | " ", 821 | "-", 822 | ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) 823 | ); 824 | $out[$key] = $value; 825 | } 826 | } 827 | } 828 | return $out; 829 | } 830 | 831 | // This function takes a input like a=b&a=c&d=e and returns the parsed 832 | // parameters like this 833 | // array('a' => array('b','c'), 'd' => 'e') 834 | public static function parse_parameters( $input ) { 835 | if (!isset($input) || !$input) return array(); 836 | 837 | $pairs = explode('&', $input); 838 | 839 | $parsed_parameters = array(); 840 | foreach ($pairs as $pair) { 841 | $split = explode('=', $pair, 2); 842 | $parameter = OAuthUtil::urldecode_rfc3986($split[0]); 843 | $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; 844 | 845 | if (isset($parsed_parameters[$parameter])) { 846 | // We have already recieved parameter(s) with this name, so add to the list 847 | // of parameters with this name 848 | 849 | if (is_scalar($parsed_parameters[$parameter])) { 850 | // This is the first duplicate, so transform scalar (string) into an array 851 | // so we can add the duplicates 852 | $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); 853 | } 854 | 855 | $parsed_parameters[$parameter][] = $value; 856 | } else { 857 | $parsed_parameters[$parameter] = $value; 858 | } 859 | } 860 | return $parsed_parameters; 861 | } 862 | 863 | public static function build_http_query($params) { 864 | if (!$params) return ''; 865 | 866 | // Urlencode both keys and values 867 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); 868 | $values = OAuthUtil::urlencode_rfc3986(array_values($params)); 869 | $params = array_combine($keys, $values); 870 | 871 | // Parameters are sorted by name, using lexicographical byte value ordering. 872 | // Ref: Spec: 9.1.1 (1) 873 | uksort($params, 'strcmp'); 874 | 875 | $pairs = array(); 876 | foreach ($params as $parameter => $value) { 877 | if (is_array($value)) { 878 | // If two or more parameters share the same name, they are sorted by their value 879 | // Ref: Spec: 9.1.1 (1) 880 | // June 12th, 2010 - changed to sort because of issue 164 by hidetaka 881 | sort($value, SORT_STRING); 882 | foreach ($value as $duplicate_value) { 883 | $pairs[] = $parameter . '=' . $duplicate_value; 884 | } 885 | } else { 886 | $pairs[] = $parameter . '=' . $value; 887 | } 888 | } 889 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 890 | // Each name-value pair is separated by an '&' character (ASCII code 38) 891 | return implode('&', $pairs); 892 | } 893 | } 894 | 895 | ?> 896 | --------------------------------------------------------------------------------