├── rebar.lock ├── doc ├── overview.edoc └── style.css ├── .gitignore ├── test ├── oauth_SUITE_data │ ├── base_string_test_A │ ├── plaintext_test_C │ ├── hmac_sha1_test_A │ ├── hmac_sha1_test_B │ ├── plaintext_test_A │ ├── plaintext_test_B │ ├── rsa_sha1_test │ ├── hmac_sha1_test_C │ ├── base_string_test_B │ ├── rsa_sha1_certificate.pem │ ├── base_string_test_C │ ├── base_string_test_D │ └── rsa_sha1_private_key.pem └── oauth_SUITE.erl ├── src ├── oauth.app.src └── oauth.erl ├── .github └── workflows │ ├── hex-publish.yml │ └── test.yml ├── THANKS.txt ├── rebar.config ├── Makefile ├── LICENSE.txt ├── CHANGELOG.md └── README.md /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /doc/overview.edoc: -------------------------------------------------------------------------------- 1 | @title eralng-oauth 2 | @doc An Erlang implementation of [https://tools.ietf.org/html/rfc5849 The OAuth 1.0 Protocol] 3 | @copyright MIT 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | *.beam 3 | ebin/ 4 | _build/ 5 | rebar3 6 | .rebar3 7 | deps 8 | .depsolver_plt 9 | doc/* 10 | !doc/style.css 11 | !doc/overview.edoc 12 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/base_string_test_A: -------------------------------------------------------------------------------- 1 | {base_string, "GET&http%3A%2F%2Fexample.com%2F&n%3Dv"}. 2 | 3 | {method, "GET"}. 4 | 5 | {url, "http://example.com/"}. 6 | 7 | {params, [{"n", "v"}]}. -------------------------------------------------------------------------------- /test/oauth_SUITE_data/plaintext_test_C: -------------------------------------------------------------------------------- 1 | % cf. http://oauth.net/core/1.0/#rfc.section.9.4.1 2 | 3 | {signature, "djr9rjt0jd78jf88&"}. 4 | 5 | {consumer, {"", "djr9rjt0jd78jf88", plaintext}}. 6 | 7 | {token_secret, ""}. 8 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/hmac_sha1_test_A: -------------------------------------------------------------------------------- 1 | % cf. http://wiki.oauth.net/TestCases 2 | 3 | {signature, "egQqG5AJep5sJ7anhXju1unge2I="}. 4 | 5 | {base_string, "bs"}. 6 | 7 | {consumer, {"", "cs", hmac_sha1}}. 8 | 9 | {token_secret, ""}. -------------------------------------------------------------------------------- /test/oauth_SUITE_data/hmac_sha1_test_B: -------------------------------------------------------------------------------- 1 | % cf. http://wiki.oauth.net/TestCases 2 | 3 | {signature, "VZVjXceV7JgPq/dOTnNmEfO0Fv8="}. 4 | 5 | {base_string, "bs"}. 6 | 7 | {consumer, {"", "cs", hmac_sha1}}. 8 | 9 | {token_secret, "ts"}. -------------------------------------------------------------------------------- /test/oauth_SUITE_data/plaintext_test_A: -------------------------------------------------------------------------------- 1 | % cf. http://oauth.net/core/1.0/#rfc.section.9.4.1 2 | 3 | {signature, "djr9rjt0jd78jf88&jjd999tj88uiths3"}. 4 | 5 | {consumer, {"", "djr9rjt0jd78jf88", plaintext}}. 6 | 7 | {token_secret, "jjd999tj88uiths3"}. 8 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/plaintext_test_B: -------------------------------------------------------------------------------- 1 | % cf. http://oauth.net/core/1.0/#rfc.section.9.4.1 2 | 3 | {signature, "djr9rjt0jd78jf88&jjd99%24tj88uiths3"}. 4 | 5 | {consumer, {"", "djr9rjt0jd78jf88", plaintext}}. 6 | 7 | {token_secret, "jjd99$tj88uiths3"}. 8 | -------------------------------------------------------------------------------- /src/oauth.app.src: -------------------------------------------------------------------------------- 1 | {application, oauth, [ 2 | {description, "An Erlang OAuth 1.0 implementation"}, 3 | {vsn, git}, 4 | {modules, [oauth]}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib, crypto, public_key, inets]}, 7 | {doc, "doc"}, 8 | {licenses, ["MIT"]}, 9 | {links, [{"GitHub", "https://github.com/erlangpack/erlang-oauth"}]} 10 | ]}. 11 | -------------------------------------------------------------------------------- /.github/workflows/hex-publish.yml: -------------------------------------------------------------------------------- 1 | name: Hex Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out 13 | uses: actions/checkout@v2 14 | 15 | - name: Publish to Hex.pm 16 | uses: erlangpack/github-action@v1 17 | env: 18 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 19 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/rsa_sha1_test: -------------------------------------------------------------------------------- 1 | {signature, "jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE="}. 2 | 3 | {base_string, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacaction.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3D13917289812797014437%26oauth_signature_method%3DRSA-SHA1%26oauth_timestamp%3D1196666512%26oauth_version%3D1.0%26size%3Doriginal"}. 4 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/hmac_sha1_test_C: -------------------------------------------------------------------------------- 1 | % cf. http://wiki.oauth.net/TestCases 2 | 3 | {signature, "tR3+Ty81lMeYAr/Fid0kMTYa/WM="}. 4 | 5 | {base_string, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"}. 6 | 7 | {consumer, {"", "kd94hf93k423kf44", hmac_sha1}}. 8 | 9 | {token_secret, "pfkkdhi9sl3r4s00"}. -------------------------------------------------------------------------------- /test/oauth_SUITE_data/base_string_test_B: -------------------------------------------------------------------------------- 1 | {base_string, "POST&https%3A%2F%2Fphotos.example.net%2Frequest_token&oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dhsu94j3884jdopsl%26oauth_signature_method%3DPLAINTEXT%26oauth_timestamp%3D1191242090%26oauth_version%3D1.0"}. 2 | 3 | {method, "POST"}. 4 | 5 | {url, "https://photos.example.net/request_token"}. 6 | 7 | {params, [ 8 | {"oauth_version", "1.0"}, 9 | {"oauth_consumer_key", "dpf43f3p2l4k3l03"}, 10 | {"oauth_timestamp", "1191242090"}, 11 | {"oauth_nonce", "hsu94j3884jdopsl"}, 12 | {"oauth_signature_method", "PLAINTEXT"} 13 | ]}. -------------------------------------------------------------------------------- /THANKS.txt: -------------------------------------------------------------------------------- 1 | Thanks to the following for patches and suggestions: 2 | 3 | * András Veres-Szentkirályi 4 | 5 | * Дамјан Георгиевски 6 | 7 | * Benoit Chesneau 8 | 9 | * Fernando Benavides 10 | 11 | * Jan Lehnardt 12 | 13 | * Jason Davies 14 | 15 | * Jebu Ittiachen 16 | 17 | * Paul Bonser 18 | 19 | * Roberto Aloi 20 | 21 | * Ryan Flynn 22 | 23 | * Sebastian Borrazas 24 | 25 | * naoya_t 26 | 27 | * narugami 28 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/rsa_sha1_certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0 3 | IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV 4 | BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 5 | gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY 6 | zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb 7 | mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3 8 | DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d 9 | 4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb 10 | WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J 11 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /test/oauth_SUITE_data/base_string_test_C: -------------------------------------------------------------------------------- 1 | {base_string, "GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal"}. 2 | 3 | {method, "GET"}. 4 | 5 | {url, "http://photos.example.net/photos"}. 6 | 7 | {params, [ 8 | {"file", "vacation.jpg"}, 9 | {"size", "original"}, 10 | {"oauth_version", "1.0"}, 11 | {"oauth_consumer_key", "dpf43f3p2l4k3l03"}, 12 | {"oauth_token", "nnch734d00sl2jdk"}, 13 | {"oauth_timestamp", "1191242096"}, 14 | {"oauth_nonce", "kllo9940pd9333jh"}, 15 | {"oauth_signature_method", "HMAC-SHA1"} 16 | ]}. -------------------------------------------------------------------------------- /test/oauth_SUITE_data/base_string_test_D: -------------------------------------------------------------------------------- 1 | % cf. http://tools.ietf.org/html/rfc5849#section-3.4.1.1 2 | 3 | {base_string, "POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk9d7dh3k39sjv7"}. 4 | 5 | {method, "POST"}. 6 | 7 | {url, "http://example.com/request"}. 8 | 9 | {params, [ 10 | {"b5", "=%3D"} 11 | , {"a3", "a"} 12 | , {"c@", ""} 13 | , {"a2", "r b"} 14 | , {"oauth_consumer_key", "9djdj82h48djs9d2"} 15 | , {"oauth_token", "kkk9d7dh3k39sjv7"} 16 | , {"oauth_signature_method", "HMAC-SHA1"} 17 | , {"oauth_timestamp", "137131201"} 18 | , {"oauth_nonce", "7d8f3e4a"} 19 | , {"c2", ""} 20 | , {"a3", "2 q"} 21 | ]}. 22 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% 2 | %% rebar configuration file (https://github.com/rebar/rebar) 3 | %% 4 | 5 | {require_min_otp_vsn, "21"}. 6 | 7 | {erl_opts, [ 8 | debug_info, 9 | fail_on_warning 10 | ] 11 | }. 12 | 13 | {profiles, [ 14 | {test, [ 15 | {xref_checks, [ 16 | undefined_function_calls, 17 | locals_not_used, 18 | deprecated_function_calls 19 | ]}, 20 | {xref_ignores, [ 21 | ]} 22 | 23 | ]}, 24 | {edoc_private, [ 25 | {edoc_opts, [ 26 | {private, true} 27 | ]} 28 | ]}, 29 | {check, [ 30 | {dialyzer, [ 31 | {warnings, [ 32 | no_return 33 | ]} 34 | ]}, 35 | 36 | {erl_opts, [ 37 | debug_info 38 | ]} 39 | ]} 40 | ]}. 41 | 42 | 43 | {edoc_opts, [ 44 | {preprocess, true}, 45 | {stylesheet, "style.css"}, 46 | {pretty_printer, erl_pp} 47 | ]}. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR := ./rebar3 2 | REBAR_URL := https://s3.amazonaws.com/rebar3/rebar3 3 | ERL ?= erl 4 | 5 | .PHONY: all compile shell test clean xref dialyzer 6 | 7 | all: compile 8 | 9 | compile: $(REBAR) 10 | $(REBAR) compile 11 | 12 | shell: $(REBAR) 13 | $(REBAR) shell 14 | 15 | test: $(REBAR) 16 | $(REBAR) as test ct 17 | 18 | clean: $(REBAR) clean_doc 19 | $(REBAR) clean 20 | 21 | clean_doc: 22 | @rm -f doc/*.html 23 | @rm -f doc/erlang.png 24 | @rm -f doc/edoc-info 25 | 26 | xref: $(REBAR) 27 | $(REBAR) as test xref 28 | 29 | dialyzer: $(REBAR) 30 | $(REBAR) as check dialyzer 31 | 32 | doc: $(REBAR) 33 | $(REBAR) edoc 34 | 35 | doc_private: $(REBAR) 36 | $(REBAR) as doc_private edoc 37 | 38 | ./rebar3: 39 | $(ERL) -noshell -s inets -s ssl \ 40 | -eval '{ok, saved_to_file} = httpc:request(get, {"$(REBAR_URL)", []}, [], [{stream, "./rebar3"}])' \ 41 | -s init stop 42 | chmod +x ./rebar3 43 | -------------------------------------------------------------------------------- /test/oauth_SUITE_data/rsa_sha1_private_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3 3 | gYytUvtC2JlYzypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp 4 | /IpH7kH41EtbmUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQAB 5 | AoGAWFlbZXlM2r5G6z48tE+RTKLvB1/btgAtq8vLw/5e3KnnbcDD6fZO07m4DRaP 6 | jRryrJdsp8qazmUdcY0O1oK4FQfpprknDjP+R1XHhbhkQ4WEwjmxPstZMUZaDWF5 7 | 8d3otc23mCzwh3YcUWFu09KnMpzZsK59OfyjtkS44EDWpbECQQDXgN0ODboKsuEA 8 | VAhAtPUqspU9ivRa6yLai9kCnPb9GcztrsJZQm4NHcKVbmD2F2L4pDRx4Pmglhfl 9 | V7G/a6T7AkEA1kfU0+DkXc6I/jXHJ6pDLA5s7dBHzWgDsBzplSdkVQbKT3MbeYje 10 | ByOxzXhulOWLBQW/vxmW4HwU95KTRlj06QJASPoBYY3yb0cN/J94P/lHgJMDCNky 11 | UEuJ/PoYndLrrN/8zow8kh91xwlJ6HJ9cTiQMmTgwaOOxPuu0eI1df4M2wJBAJJS 12 | WrKUT1z/O+zbLDOZwGTFNPzvzRgmft4z4A1J6OlmyZ+XKpvDKloVtcRpCJoEZPn5 13 | AwaroquID4k/PfI7rIECQHeWa6+kPADv9IrK/92mujujS0MSEiynDw5NjTnHAH0v 14 | 8TrXzs+LCWDN/gbOCKPfnWRkgwgOeC8NN3h0zUIIUtA= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks the tests and dialyzer. 2 | 3 | name: Test 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | linux: 16 | name: Test on OTP ${{ matrix.otp_version }} 17 | runs-on: ${{ matrix.os }} 18 | 19 | strategy: 20 | matrix: 21 | otp_version: [22.3, 23.3, 24.0.5] 22 | os: [ubuntu-latest] 23 | 24 | container: 25 | image: erlang:${{ matrix.otp_version }} 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Compile 30 | run: make 31 | - name: Test 32 | run: make test 33 | - name: XRef 34 | run: make xref 35 | - name: Dialyzer 36 | run: make dialyzer 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2021 Tim Fletcher 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /doc/style.css: -------------------------------------------------------------------------------- 1 | /* standard EDoc style sheet */ 2 | body { 3 | font-family: Verdana, Arial, Helvetica, sans-serif; 4 | margin-left: .25in; 5 | margin-right: .2in; 6 | margin-top: 0.2in; 7 | margin-bottom: 0.2in; 8 | color: #000000; 9 | background-color: #ffffff; 10 | } 11 | h1,h2 { 12 | margin-left: -0.2in; 13 | } 14 | div.navbar { 15 | background-color: #add8e6; 16 | padding: 0.2em; 17 | } 18 | h2.indextitle { 19 | padding: 0.4em; 20 | background-color: #add8e6; 21 | } 22 | h3.function,h3.typedecl { 23 | background-color: #add8e6; 24 | padding-left: 1em; 25 | } 26 | div.spec { 27 | margin-left: 2em; 28 | 29 | background-color: #eeeeee; 30 | } 31 | a.module { 32 | text-decoration:none 33 | } 34 | a.module:hover { 35 | background-color: #eeeeee; 36 | } 37 | ul.definitions { 38 | list-style-type: none; 39 | } 40 | ul.index { 41 | list-style-type: none; 42 | background-color: #eeeeee; 43 | } 44 | 45 | /* 46 | * Minor style tweaks 47 | */ 48 | ul { 49 | list-style-type: square; 50 | } 51 | table { 52 | border-collapse: collapse; 53 | } 54 | td { 55 | padding: 3px; 56 | vertical-align: middle; 57 | } 58 | 59 | /* 60 | Tune styles 61 | */ 62 | 63 | table[summary="navigation bar"] { 64 | background-image: url('http://zotonic.com/lib/images/logo.png'); 65 | background-repeat: no-repeat; 66 | background-position: center; 67 | } 68 | 69 | code, p>tt, a>tt { 70 | font-size: 1.2em; 71 | } 72 | 73 | p { 74 | line-height: 1.5; 75 | } 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.1.0 2 | 3 | * rebar3 build tool 4 | * reorganize tests to use CT and rebar3 5 | 6 | 7 | # 2.0.0 8 | 9 | * **Erlang/OTP 21 or greater now required** 10 | * Added support for crypto:mac/4 in OTP 22+ 11 | * Added delete/3, delete/5, and delete/6 functions 12 | * Removed uri_params_encode/1 function (use uri_string:compose_query/1 instead) 13 | * Removed uri_params_decode/1 function (use uri_string:dissect_query/1 instead) 14 | 15 | 16 | # 1.6.0 17 | 18 | * Switched to using crypto:hmac/3 19 | * Switched to using crypto:strong_rand_bytes/1 20 | * Exported oauth:signature/5 function 21 | * Erlang/OTP R16B03 or greater now required 22 | 23 | 24 | # 1.5.0 25 | 26 | * Added support for encoding binary terms as parameter values 27 | 28 | 29 | # 1.4.0 30 | 31 | * Added support for new crypto:hmac/3 function 32 | * Moved unit tests from github.com/tim/erlang-oauth-tests 33 | 34 | 35 | # 1.3.0 36 | 37 | * Added oauth:put/6 and oauth:put/7 functions 38 | 39 | 40 | # 1.2.2 41 | 42 | * Added support for new tagged tuple returned by http_uri:parse/1 (R15B) 43 | 44 | 45 | # 1.2.1 46 | 47 | * Updated to use a constant time algorithm to compare signature strings 48 | 49 | 50 | # 1.2.0 51 | 52 | * Added oauth:get/3 and oauth:post/3 functions 53 | * Collapsed into just a single module 54 | 55 | 56 | # 1.1.1 57 | 58 | * Updated to use the correct request parameter normalization algorithm 59 | 60 | 61 | # 1.1.0 62 | 63 | * Updated to use the new public key API introduced in R14B (public_key-0.8) 64 | 65 | 66 | # 1.0.2 67 | 68 | * Added oauth:get/6 and oauth:post/6 with additional HttpcOptions parameter 69 | 70 | 71 | # 1.0.1 72 | 73 | * First version numbered version! 74 | -------------------------------------------------------------------------------- /test/oauth_SUITE.erl: -------------------------------------------------------------------------------- 1 | %% -*- coding: utf-8 -*- 2 | %% ------------------------------------------------------------------- 3 | %% 4 | %% Copyright (c) 2021 Marc Worrell 5 | %% 6 | %% ------------------------------------------------------------------- 7 | 8 | -module(oauth_SUITE). 9 | -compile(export_all). 10 | 11 | -include_lib("common_test/include/ct.hrl"). 12 | -include_lib("eunit/include/eunit.hrl"). 13 | 14 | %% ------------------------------------------------------------ 15 | %% Tests list 16 | %% ------------------------------------------------------------ 17 | 18 | all() -> 19 | [ 20 | signature_base_string, 21 | plaintext, 22 | hmac_sha1, 23 | rsa_sha1 24 | ]. 25 | 26 | %% ------------------------------------------------------------ 27 | %% Init & clean 28 | %% ------------------------------------------------------------ 29 | 30 | init_per_suite(Config) -> 31 | Config. 32 | 33 | end_per_suite(_Config) -> 34 | ok. 35 | 36 | init_per_testcase(_TestCase, Config) -> 37 | Config. 38 | 39 | end_per_testcase(_TestCase, _Config) -> 40 | ok. 41 | 42 | %% ------------------------------------------------------------ 43 | %% Test cases 44 | %% ------------------------------------------------------------ 45 | 46 | 47 | signature_base_string(Config) -> 48 | test_with( 49 | Config, 50 | "base_string_test_*", 51 | [method, url, params, base_string], 52 | fun (Method, URL, Params, BaseString) -> 53 | [?_assertEqual(BaseString, oauth:signature_base_string(Method, URL, Params))] 54 | end). 55 | 56 | plaintext(Config) -> 57 | test_with( 58 | Config, 59 | "plaintext_test_*", 60 | [consumer, token_secret, signature], 61 | fun (Consumer, TokenSecret, Signature) -> 62 | SignatureTest = ?_assertEqual(Signature, oauth:plaintext_signature(Consumer, TokenSecret)), 63 | VerifyTest = ?_assertEqual(true, oauth:plaintext_verify(Signature, Consumer, TokenSecret)), 64 | [SignatureTest, VerifyTest] 65 | end). 66 | 67 | hmac_sha1(Config) -> 68 | test_with( 69 | Config, 70 | "hmac_sha1_test_*", 71 | [base_string, consumer, token_secret, signature], 72 | fun (BaseString, Consumer, TokenSecret, Signature) -> 73 | SignatureTest = ?_assertEqual(Signature, oauth:hmac_sha1_signature(BaseString, Consumer, TokenSecret)), 74 | VerifyTest = ?_assertEqual(true, oauth:hmac_sha1_verify(Signature, BaseString, Consumer, TokenSecret)), 75 | [SignatureTest, VerifyTest] 76 | end). 77 | 78 | rsa_sha1(Config) -> 79 | Pkey = data_path(Config, "rsa_sha1_private_key.pem"), 80 | Cert = data_path(Config, "rsa_sha1_certificate.pem"), 81 | [BaseString, Signature] = read([base_string, signature], data_path(Config, "rsa_sha1_test")), 82 | SignatureTest = ?_assertEqual(Signature, oauth:rsa_sha1_signature(BaseString, {"", Pkey, rsa_sha1})), 83 | VerifyTest = ?_assertEqual(true, oauth:rsa_sha1_verify(Signature, BaseString, {"", Cert, rsa_sha1})), 84 | [SignatureTest, VerifyTest]. 85 | 86 | test_with(Config, FilenamePattern, Keys, Fun) -> 87 | lists:flatten( 88 | lists:map( 89 | fun (Path) -> apply(Fun, read(Keys, Path)) end, 90 | filelib:wildcard(data_path(Config, FilenamePattern)))). 91 | 92 | data_path(Config, Basename) -> 93 | DataDir = ?config(data_dir, Config), 94 | filename:join([DataDir, Basename]). 95 | 96 | read(Keys, Path) -> 97 | {ok, Proplist} = file:consult(Path), 98 | [ proplists:get_value(K, Proplist) || K <- Keys ]. 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][gh badge]][gh] 2 | [![Hex.pm version][hexpm version]][hexpm] 3 | [![Hex.pm Downloads][hexpm downloads]][hexpm] 4 | [![Hex.pm Documentation][hexdocs documentation]][hexdocs] 5 | [![Erlang Versions][erlang version badge]][gh] 6 | [![License][license]](LICENSE.txt) 7 | 8 | # erlang-oauth 9 | 10 | An Erlang implementation of [The OAuth 1.0 Protocol](https://tools.ietf.org/html/rfc5849). 11 | 12 | There are functions for 13 | - generating signatures (*client* side), 14 | - verifying signatures (*server* side), 15 | - some convenience functions for making OAuth HTTP requests (*client* side). 16 | 17 | ## Usage 18 | 19 | Erlang-oauth is on Hex, you can use the package by adding it into your rebar.config: 20 | 21 | {deps, [ 22 | {oauth, "2.1.0"} 23 | ]}. 24 | 25 | 26 | ## Erlang/OTP compatibility 27 | 28 | Erlang/OTP 21 or greater. 29 | 30 | 31 | ## Quick start (client usage) 32 | 33 | $ erl -make 34 | Recompile: src/oauth 35 | $ erl -pa ebin -s crypto -s inets 36 | ... 37 | 1> Consumer = {"key", "secret", hmac_sha1}. 38 | ... 39 | 2> RequestTokenURL = "http://term.ie/oauth/example/request_token.php". 40 | ... 41 | 3> {ok, RequestTokenResponse} = oauth:get(RequestTokenURL, [], Consumer). 42 | ... 43 | 4> RequestTokenParams = oauth:params_decode(RequestTokenResponse). 44 | ... 45 | 5> RequestToken = oauth:token(RequestTokenParams). 46 | ... 47 | 6> RequestTokenSecret = oauth:token_secret(RequestTokenParams). 48 | ... 49 | 7> AccessTokenURL = "http://term.ie/oauth/example/access_token.php". 50 | ... 51 | 8> {ok, AccessTokenResponse} = oauth:get(AccessTokenURL, [], Consumer, RequestToken, RequestTokenSecret). 52 | ... 53 | 9> AccessTokenParams = oauth:params_decode(AccessTokenResponse). 54 | ... 55 | 10> AccessToken = oauth:token(AccessTokenParams). 56 | ... 57 | 11> AccessTokenSecret = oauth:token_secret(AccessTokenParams). 58 | ... 59 | 12> URL = "http://term.ie/oauth/example/echo_api.php". 60 | ... 61 | 13> {ok, Response} = oauth:get(URL, [{"hello", "world"}], Consumer, AccessToken, AccessTokenSecret). 62 | ... 63 | 14> oauth:params_decode(Response). 64 | ... 65 | 66 | 67 | ## OAuth consumer representation 68 | 69 | Consumers are represented using tuples: 70 | 71 | ```erlang 72 | {Key::string(), Secret::string(), plaintext} 73 | 74 | {Key::string(), Secret::string(), hmac_sha1} 75 | 76 | {Key::string(), RSAPrivateKeyPath::string(), rsa_sha1} % client side 77 | 78 | {Key::string(), RSACertificatePath::string(), rsa_sha1} % server side 79 | ``` 80 | 81 | 82 | ## Other notes 83 | 84 | This implementation should be compatible with the signature algorithms 85 | presented in [RFC5849 - The OAuth 1.0 Protocol](http://tools.ietf.org/html/rfc5849), 86 | and [OAuth Core 1.0 Revision A](http://oauth.net/core/1.0a/). It is *not* intended 87 | to cover [OAuth 2.0](http://oauth.net/2/). 88 | 89 | This is *not* a "plug and play" server implementation. In order to implement OAuth 90 | correctly as a provider you have more work to do: token storage, nonce and timestamp 91 | verification etc. 92 | 93 | This is *not* a "bells and whistles" HTTP client. If you need fine grained control 94 | over your HTTP requests or you prefer to use something other than inets/httpc then you 95 | will need to assemble the requests yourself. Use `oauth:sign/6` to generate a list of 96 | signed OAuth parameters, and then either `oauth:uri_params_encode/1` or `oauth:header_params_encode/1` 97 | to encode the signed parameters. 98 | 99 | The percent encoding/decoding implementations are based on [ibrowse](https://github.com/cmullaparthi/ibrowse) 100 | 101 | 102 | ## License 103 | 104 | This project is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT). 105 | 106 | 107 | [hexpm]: https://hex.pm/packages/oauth 108 | [hexpm version]: https://img.shields.io/hexpm/v/oauth.svg?style=flat-curcle "Hex version" 109 | [hexpm downloads]: https://img.shields.io/hexpm/dt/oauth.svg?style=flat-curcle 110 | [hexdocs documentation]: https://img.shields.io/badge/hex-docs-purple.svg?style=flat-curcle 111 | [hexdocs]: https://hexdocs.pm/oauth 112 | [gh]: https://github.com/erlangpack/erlang-oauth/actions/workflows/test.yaml 113 | [gh badge]: https://github.com/erlangpack/erlang-oauth/workflows/Test/badge.svg 114 | [erlang version badge]: https://img.shields.io/badge/Supported%20Erlang%2FOTP-21%20to%2023-blue.svg?style=flat-curcle 115 | [license]: https://img.shields.io/badge/License-MIT-blue.svg "MIT" 116 | -------------------------------------------------------------------------------- /src/oauth.erl: -------------------------------------------------------------------------------- 1 | % Copyright (c) 2008-2021 Tim Fletcher 2 | % 3 | % Permission is hereby granted, free of charge, to any person obtaining 4 | % a copy of this software and associated documentation files (the 5 | % "Software"), to deal in the Software without restriction, including 6 | % without limitation the rights to use, copy, modify, merge, publish, 7 | % distribute, sublicense, and/or sell copies of the Software, and to 8 | % permit persons to whom the Software is furnished to do so, subject to 9 | % the following conditions: 10 | % 11 | % The above copyright notice and this permission notice shall be 12 | % included in all copies or substantial portions of the Software. 13 | % 14 | % THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | % EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | % MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | % NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | % LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | % OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | % WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -module(oauth). 23 | 24 | -export([get/3, get/5, get/6, post/3, post/5, post/6, delete/3, delete/5, delete/6, put/6, put/7]). 25 | 26 | -export([uri/2, header/1, sign/6, params_decode/1, token/1, token_secret/1, verify/6]). 27 | 28 | -export([plaintext_signature/2, hmac_sha1_signature/5, 29 | hmac_sha1_signature/3, rsa_sha1_signature/4, rsa_sha1_signature/2, 30 | signature_base_string/3, params_encode/1, signature/5]). 31 | 32 | -export([plaintext_verify/3, hmac_sha1_verify/6, hmac_sha1_verify/4, 33 | rsa_sha1_verify/5, rsa_sha1_verify/3]). 34 | 35 | -export([header_params_encode/1, header_params_decode/1]). 36 | 37 | -include_lib("public_key/include/public_key.hrl"). 38 | 39 | -type signature_method() :: plaintext | hmac_sha1 | rsa_sha1. 40 | %%
    41 | %%
  • `PLAINTEXT' is a simple method for a more efficient implementation which offloads 42 | %% most of the security requirements to the HTTPS layer.
  • 43 | %%
  • `HMAC-SHA1' offers a simple and common algorithm that is available on most platforms 44 | %% but not on all legacy devices and uses a symmetric shared secret.
  • 45 | %%
  • `RSA-SHA1' provides enhanced security using key-pairs but is more complex and 46 | %% requires key generation and a longer learning curve.
  • 47 | %%
48 | -export_type([ 49 | signature_method/0 50 | ]). 51 | 52 | -if(?OTP_RELEASE >= 22). 53 | -define(HMAC_SHA1(Key, Data), crypto:mac(hmac, sha, Key, Data)). 54 | -else. 55 | -define(HMAC_SHA1(Key, Data), crypto:hmac(sha, Key, Data)). 56 | -endif. 57 | 58 | %% @doc Send request using HTTP-method GET. `Token' and `TokenSecret' values are empty string. 59 | %% @param URL server URL 60 | %% @param ExtraParams signature params 61 | %% @param Consumer client information 62 | %% @equiv get(URL, ExtraParams, Consumer, "", "") 63 | 64 | -spec get(URL, ExtraParams, Consumer) -> Result when 65 | URL :: httpc:url(), 66 | ExtraParams :: list(), 67 | Consumer :: {Key, Secret, Method}, 68 | Key :: string(), 69 | Secret :: string(), 70 | Method :: signature_method(), 71 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 72 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 73 | Body :: httpc:http_string() | binary(), 74 | Reason :: term(). 75 | get(URL, ExtraParams, Consumer) -> 76 | get(URL, ExtraParams, Consumer, "", ""). 77 | 78 | %% @doc Send request using HTTP-method GET. 79 | %% @param URL server URL 80 | %% @param ExtraParams signature params 81 | %% @param Consumer client information 82 | %% @param Token sign token 83 | %% @param TokenSecret sign token secret 84 | %% @equiv get(URL, ExtraParams, Consumer, Token, TokenSecret, []) 85 | 86 | -spec get(URL, ExtraParams, Consumer, Token, TokenSecret) -> Result when 87 | URL :: httpc:url(), 88 | ExtraParams :: list(), 89 | Consumer :: {Key, Secret, Method}, 90 | Key :: string(), 91 | Secret :: string(), 92 | Method :: signature_method(), 93 | Token :: string(), 94 | TokenSecret :: string(), 95 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 96 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 97 | Body :: httpc:http_string() | binary(), 98 | Reason :: term(). 99 | get(URL, ExtraParams, Consumer, Token, TokenSecret) -> 100 | get(URL, ExtraParams, Consumer, Token, TokenSecret, []). 101 | 102 | %% @doc Send request using HTTP-method GET. 103 | %% @param URL server URL 104 | %% @param ExtraParams signature params 105 | %% @param Consumer client information 106 | %% @param Token sign token 107 | %% @param TokenSecret sign token secret 108 | %% @param HttpcOptions HTTP options 109 | 110 | -spec get(URL, ExtraParams, Consumer, Token, TokenSecret, HttpcOptions) -> Result when 111 | URL :: httpc:url(), 112 | ExtraParams :: list(), 113 | Consumer :: {Key, Secret, Method}, 114 | Key :: string(), 115 | Secret :: string(), 116 | Method :: signature_method(), 117 | Token :: string(), 118 | TokenSecret :: string(), 119 | HttpcOptions :: [Option], 120 | Option :: {sync, boolean()} | {stream, StreamTo} | {body_format, BodyFormat} | 121 | {full_result, boolean()} | {headers_as_is, boolean()} | {socket_opts, SocketOpts} | 122 | {receiver, Receiver}, 123 | StreamTo :: none | self | {self, once} | httpc:filename(), 124 | BodyFormat :: 'string' | 'binary', 125 | SocketOpts :: [httpc:socket_opt()], 126 | Receiver :: Receiver :: pid() | function() | {Module, Function, Args}, 127 | Module :: atom(), 128 | Function :: atom(), 129 | Args :: list(), 130 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 131 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 132 | Body :: httpc:http_string() | binary(), 133 | Reason :: term(). 134 | get(URL, ExtraParams, Consumer, Token, TokenSecret, HttpcOptions) -> 135 | SignedParams = sign("GET", URL, ExtraParams, Consumer, Token, TokenSecret), 136 | http_request(get, {uri(URL, SignedParams), []}, HttpcOptions). 137 | 138 | %% @doc Send request using HTTP-method POST. 139 | %% @equiv post(URL, ExtraParams, Consumer, "", "") 140 | 141 | -spec post(URL, ExtraParams, Consumer) -> Result when 142 | URL :: httpc:url(), 143 | ExtraParams :: list(), 144 | Consumer :: {Key, Secret, Method}, 145 | Key :: string(), 146 | Secret :: string(), 147 | Method :: signature_method(), 148 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 149 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 150 | Body :: httpc:http_string() | binary(), 151 | Reason :: term(). 152 | post(URL, ExtraParams, Consumer) -> 153 | post(URL, ExtraParams, Consumer, "", ""). 154 | 155 | %% @doc Send request using HTTP-method POST. 156 | %% @equiv post(URL, ExtraParams, Consumer, Token, TokenSecret, []) 157 | 158 | -spec post(URL, ExtraParams, Consumer, Token, TokenSecret) -> Result when 159 | URL :: httpc:url(), 160 | ExtraParams :: list(), 161 | Consumer :: {Key, Secret, Method}, 162 | Key :: string(), 163 | Secret :: string(), 164 | Method :: signature_method(), 165 | Token :: string(), 166 | TokenSecret :: string(), 167 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 168 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 169 | Body :: httpc:http_string() | binary(), 170 | Reason :: term(). 171 | post(URL, ExtraParams, Consumer, Token, TokenSecret) -> 172 | post(URL, ExtraParams, Consumer, Token, TokenSecret, []). 173 | 174 | %% @doc Send request using HTTP-method POST. 175 | 176 | -spec post(URL, ExtraParams, Consumer, Token, TokenSecret, HttpcOptions) -> Result when 177 | URL :: httpc:url(), 178 | ExtraParams :: list(), 179 | Consumer :: {Key, Secret, Method}, 180 | Key :: string(), 181 | Secret :: string(), 182 | Method :: signature_method(), 183 | Token :: string(), 184 | TokenSecret :: string(), 185 | HttpcOptions :: [Option], 186 | Option :: {sync, boolean()} | {stream, StreamTo} | {body_format, BodyFormat} | 187 | {full_result, boolean()} | {headers_as_is, boolean()} | {socket_opts, SocketOpts} | 188 | {receiver, Receiver}, 189 | StreamTo :: none | self | {self, once} | httpc:filename(), 190 | BodyFormat :: 'string' | 'binary', 191 | SocketOpts :: [httpc:socket_opt()], 192 | Receiver :: Receiver :: pid() | function() | {Module, Function, Args}, 193 | Module :: atom(), 194 | Function :: atom(), 195 | Args :: list(), 196 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 197 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 198 | Body :: httpc:http_string() | binary(), 199 | Reason :: term(). 200 | post(URL, ExtraParams, Consumer, Token, TokenSecret, HttpcOptions) -> 201 | SignedParams = sign("POST", URL, ExtraParams, Consumer, Token, TokenSecret), 202 | http_request(post, {URL, [], "application/x-www-form-urlencoded", uri_string:compose_query(SignedParams)}, HttpcOptions). 203 | 204 | %% @doc Send request using HTTP-method DELETE. 205 | %% @equiv delete(URL, ExtraParams, Consumer, "", "") 206 | 207 | -spec delete(URL, ExtraParams, Consumer) -> Result when 208 | URL :: httpc:url(), 209 | ExtraParams :: list(), 210 | Consumer :: {Key, Secret, Method}, 211 | Key :: string(), 212 | Secret :: string(), 213 | Method :: signature_method(), 214 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 215 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 216 | Body :: httpc:http_string() | binary(), 217 | Reason :: term(). 218 | delete(URL, ExtraParams, Consumer) -> 219 | delete(URL, ExtraParams, Consumer, "", ""). 220 | 221 | %% @doc Send request using HTTP-method DELETE. 222 | %% @equiv delete(URL, ExtraParams, Consumer, Token, TokenSecret, []) 223 | 224 | -spec delete(URL, ExtraParams, Consumer, Token, TokenSecret) -> Result when 225 | URL :: httpc:url(), 226 | ExtraParams :: list(), 227 | Consumer :: {Key, Secret, Method}, 228 | Key :: string(), 229 | Secret :: string(), 230 | Method :: signature_method(), 231 | Token :: string(), 232 | TokenSecret :: string(), 233 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 234 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 235 | Body :: httpc:http_string() | binary(), 236 | Reason :: term(). 237 | delete(URL, ExtraParams, Consumer, Token, TokenSecret) -> 238 | delete(URL, ExtraParams, Consumer, Token, TokenSecret, []). 239 | 240 | %% @doc Send request using HTTP-method DELETE. 241 | 242 | -spec delete(URL, ExtraParams, Consumer, Token, TokenSecret, HttpcOptions) -> Result when 243 | URL :: httpc:url(), 244 | ExtraParams :: list(), 245 | Consumer :: {Key, Secret, Method}, 246 | Key :: string(), 247 | Secret :: string(), 248 | Method :: signature_method(), 249 | Token :: string(), 250 | TokenSecret :: string(), 251 | HttpcOptions :: [Option], 252 | Option :: {sync, boolean()} | {stream, StreamTo} | {body_format, BodyFormat} | 253 | {full_result, boolean()} | {headers_as_is, boolean()} | {socket_opts, SocketOpts} | 254 | {receiver, Receiver}, 255 | StreamTo :: none | self | {self, once} | httpc:filename(), 256 | BodyFormat :: 'string' | 'binary', 257 | SocketOpts :: [httpc:socket_opt()], 258 | Receiver :: Receiver :: pid() | function() | {Module, Function, Args}, 259 | Module :: atom(), 260 | Function :: atom(), 261 | Args :: list(), 262 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 263 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 264 | Body :: httpc:http_string() | binary(), 265 | Reason :: term(). 266 | delete(URL, ExtraParams, Consumer, Token, TokenSecret, HttpcOptions) -> 267 | SignedParams = sign("DELETE", URL, ExtraParams, Consumer, Token, TokenSecret), 268 | http_request(delete, {URL, [], "application/x-www-form-urlencoded", uri_string:compose_query(SignedParams)}, HttpcOptions). 269 | 270 | %% @doc Send request using HTTP-method PUT. 271 | %% @equiv put(URL, ExtraParams, {ContentType, Body}, Consumer, Token, TokenSecret, []) 272 | 273 | -spec put(URL, ExtraParams, {ContentType, Body}, Consumer, Token, TokenSecret) -> Result when 274 | URL :: httpc:url(), 275 | ExtraParams :: list(), 276 | ContentType :: httpc:content_type(), 277 | Body :: httpc:body(), 278 | Consumer :: {Key, Secret, Method}, 279 | Key :: string(), 280 | Secret :: string(), 281 | Method :: signature_method(), 282 | Token :: string(), 283 | TokenSecret :: string(), 284 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 285 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 286 | Body :: httpc:http_string() | binary(), 287 | Reason :: term(). 288 | put(URL, ExtraParams, {ContentType, Body}, Consumer, Token, TokenSecret) -> 289 | put(URL, ExtraParams, {ContentType, Body}, Consumer, Token, TokenSecret, []). 290 | 291 | %% @doc Send request using HTTP-method PUT. 292 | 293 | -spec put(URL, ExtraParams, {ContentType, Body}, Consumer, Token, TokenSecret, HttpcOptions) -> Result when 294 | URL :: httpc:url(), 295 | ExtraParams :: list(), 296 | ContentType :: httpc:content_type(), 297 | Body :: httpc:body(), 298 | Consumer :: {Key, Secret, Method}, 299 | Key :: string(), 300 | Secret :: string(), 301 | Method :: signature_method(), 302 | Token :: string(), 303 | TokenSecret :: string(), 304 | HttpcOptions :: [Option], 305 | Option :: {sync, boolean()} | {stream, StreamTo} | {body_format, BodyFormat} | 306 | {full_result, boolean()} | {headers_as_is, boolean()} | {socket_opts, SocketOpts} | 307 | {receiver, Receiver}, 308 | StreamTo :: none | self | {self, once} | httpc:filename(), 309 | BodyFormat :: 'string' | 'binary', 310 | SocketOpts :: [httpc:socket_opt()], 311 | Receiver :: Receiver :: pid() | function() | {Module, Function, Args}, 312 | Module :: atom(), 313 | Function :: atom(), 314 | Args :: list(), 315 | Result :: {ok, RequestTokenResponse} | {ok, saved_to_file} | {error, Reason}, 316 | RequestTokenResponse :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 317 | Body :: httpc:http_string() | binary(), 318 | Reason :: term(). 319 | put(URL, ExtraParams, {ContentType, Body}, Consumer, Token, TokenSecret, HttpcOptions) -> 320 | SignedParams = sign("PUT", URL, ExtraParams, Consumer, Token, TokenSecret), 321 | http_request(put, {uri(URL, SignedParams), [], ContentType, Body}, HttpcOptions). 322 | 323 | %% @doc Build URI using provided parameters. 324 | -spec uri(Base, Params) -> Result when 325 | Base :: string(), 326 | Params :: QueryList, 327 | QueryList :: [{unicode:chardata(), unicode:chardata() | true}], 328 | Result :: string(). 329 | uri(Base, []) -> 330 | Base; 331 | uri(Base, Params) -> 332 | lists:concat([Base, "?", uri_string:compose_query(Params)]). 333 | 334 | %% @doc Get encode authorization paramaters. 335 | %% @returns {"Authorization", "OAuth " ++ string()} 336 | 337 | -spec header(Params) -> Result when 338 | Params :: [{Key, Value}], 339 | Key :: Term, 340 | Value :: Term, 341 | Term :: integer() | atom() | binary() | list(), 342 | Result :: {string(), string()}. 343 | header(Params) -> 344 | {"Authorization", "OAuth " ++ header_params_encode(Params)}. 345 | 346 | %% @doc Get `oauth_token'. 347 | 348 | -spec token(Params) -> Result when 349 | Params :: [term()], 350 | Result :: string(). 351 | token(Params) -> 352 | proplists:get_value("oauth_token", Params). 353 | 354 | %% @doc Get `oauth_token_secret'. 355 | 356 | -spec token_secret(Params) -> Result when 357 | Params :: [term()], 358 | Result :: string(). 359 | token_secret(Params) -> 360 | proplists:get_value("oauth_token_secret", Params). 361 | 362 | -spec consumer_key(Consumer) -> Result when 363 | Consumer :: {Key, Secret, Method}, 364 | Key :: string(), 365 | Secret :: string(), 366 | Method :: signature_method(), 367 | Result :: Key. 368 | consumer_key(_Consumer={Key, _, _}) -> 369 | Key. 370 | 371 | -spec consumer_secret(Consumer) -> Result when 372 | Consumer :: {Key, Secret, Method}, 373 | Key :: string(), 374 | Secret :: string(), 375 | Method :: signature_method(), 376 | Result :: Secret. 377 | consumer_secret(_Consumer={_, Secret, _}) -> 378 | Secret. 379 | 380 | -spec signature_method(Consumer) -> Result when 381 | Consumer :: {Key, Secret, Method}, 382 | Key :: string(), 383 | Secret :: string(), 384 | Method :: signature_method(), 385 | Result :: Method. 386 | signature_method(_Consumer={_, _, Method}) -> 387 | Method. 388 | 389 | %% @doc Get signature parameters. 390 | %% @param HttpMethod "GET" | "POST" | "DELETE" | "PUT" 391 | 392 | -spec sign(HttpMethod, URL, Params, Consumer, Token, TokenSecret) -> Result when 393 | HttpMethod :: string(), 394 | URL :: httpc:url(), 395 | Params :: [{ParamsKey, ParamsValue}], 396 | ParamsKey :: Term, 397 | ParamsValue :: Term, 398 | Consumer :: {Key, Secret, Method}, 399 | Key :: string(), 400 | Secret :: string(), 401 | Method :: signature_method(), 402 | Token :: string(), 403 | TokenSecret :: string(), 404 | Result :: [{string(),string()}]. 405 | sign(HttpMethod, URL, Params, Consumer, Token, TokenSecret) -> 406 | SignatureParams = signature_params(Consumer, Params, Token), 407 | Signature = signature(HttpMethod, URL, SignatureParams, Consumer, TokenSecret), 408 | [{"oauth_signature", Signature} | SignatureParams]. 409 | 410 | -spec signature_params(Consumer, Params, Token) -> Result when 411 | Consumer :: {Key, Secret, Method}, 412 | Key :: string(), 413 | Secret :: string(), 414 | Method :: signature_method(), 415 | Params :: [{ParamsKey, ParamsValue}], 416 | ParamsKey :: Term, 417 | ParamsValue :: Term, 418 | Term :: integer() | atom() | binary() | list(), 419 | Token :: string(), 420 | Result :: [{string(), string()}]. 421 | signature_params(Consumer, Params, "") -> 422 | signature_params(Consumer, Params); 423 | signature_params(Consumer, Params, Token) -> 424 | signature_params(Consumer, [{"oauth_token", Token} | Params]). 425 | 426 | signature_params(Consumer, Params) -> 427 | Timestamp = unix_timestamp(), 428 | Nonce = base64:encode_to_string(crypto:strong_rand_bytes(32)), % cf. ruby-oauth 429 | [ {"oauth_version", "1.0"} 430 | , {"oauth_nonce", Nonce} 431 | , {"oauth_timestamp", integer_to_list(Timestamp)} 432 | , {"oauth_signature_method", signature_method_string(Consumer)} 433 | , {"oauth_consumer_key", consumer_key(Consumer)} 434 | | Params 435 | ]. 436 | 437 | %% @doc Verify signature by provided signature method. 438 | -spec verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) -> Result when 439 | Signature :: base64:base64_string() | base64:base64_binary(), 440 | HttpMethod :: string(), 441 | URL :: httpc:url(), 442 | Params :: [{ParamsKey, ParamsValue}], 443 | ParamsKey :: Term, 444 | ParamsValue :: Term, 445 | Term :: integer() | atom() | binary() | list(), 446 | Consumer :: {Key, Secret, Method}, 447 | Key :: string(), 448 | Secret :: string(), 449 | Method :: signature_method(), 450 | TokenSecret :: string(), 451 | Result :: boolean(). 452 | verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) -> 453 | case signature_method(Consumer) of 454 | plaintext -> 455 | plaintext_verify(Signature, Consumer, TokenSecret); 456 | hmac_sha1 -> 457 | hmac_sha1_verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret); 458 | rsa_sha1 -> 459 | rsa_sha1_verify(Signature, HttpMethod, URL, Params, Consumer) 460 | end. 461 | 462 | %% @doc Get signature using provided signature method. 463 | -spec signature(HttpMethod, URL, Params, Consumer, TokenSecret) -> Result when 464 | HttpMethod :: string(), 465 | URL :: httpc:url(), 466 | Params :: [{ParamsKey, ParamsValue}], 467 | ParamsKey :: Term, 468 | ParamsValue :: Term, 469 | Term :: integer() | atom() | binary() | list(), 470 | Consumer :: {Key, Secret, Method}, 471 | Key :: string(), 472 | Secret :: string(), 473 | Method :: signature_method(), 474 | TokenSecret :: string(), 475 | Result :: base64:base64_string(). 476 | signature(HttpMethod, URL, Params, Consumer, TokenSecret) -> 477 | case signature_method(Consumer) of 478 | plaintext -> 479 | plaintext_signature(Consumer, TokenSecret); 480 | hmac_sha1 -> 481 | hmac_sha1_signature(HttpMethod, URL, Params, Consumer, TokenSecret); 482 | rsa_sha1 -> 483 | rsa_sha1_signature(HttpMethod, URL, Params, Consumer) 484 | end. 485 | 486 | %% @doc Get selected signature method. 487 | %% @returns "PLAINTEXT" | "HMAC-SHA1" | "RSA-SHA1" 488 | 489 | -spec signature_method_string(Consumer) -> Result when 490 | Consumer :: {Key, Secret, Method}, 491 | Key :: string(), 492 | Secret :: string(), 493 | Method :: signature_method(), 494 | Result :: string(). 495 | signature_method_string(Consumer) -> 496 | case signature_method(Consumer) of 497 | plaintext -> 498 | "PLAINTEXT"; 499 | hmac_sha1 -> 500 | "HMAC-SHA1"; 501 | rsa_sha1 -> 502 | "RSA-SHA1" 503 | end. 504 | 505 | %% @doc Build plain text Signature 506 | 507 | -spec plaintext_signature(Consumer, TokenSecret) -> Result when 508 | Consumer :: {Key, Secret, Method}, 509 | Key :: string(), 510 | Secret :: string(), 511 | Method :: signature_method(), 512 | TokenSecret :: string(), 513 | Result :: base64:base64_string(). 514 | plaintext_signature(Consumer, TokenSecret) -> 515 | uri_join([consumer_secret(Consumer), TokenSecret]). 516 | 517 | %% @doc Verify plain text Signature 518 | 519 | -spec plaintext_verify(Signature, Consumer, TokenSecret) -> Result when 520 | Signature :: base64:base64_string() | base64:base64_binary(), 521 | Consumer :: {Key, Secret, Method}, 522 | Key :: string(), 523 | Secret :: string(), 524 | Method :: signature_method(), 525 | TokenSecret :: string(), 526 | Result :: boolean(). 527 | plaintext_verify(Signature, Consumer, TokenSecret) -> 528 | verify_in_constant_time(Signature, plaintext_signature(Consumer, TokenSecret)). 529 | 530 | %% @doc Build HMAC-SHA1 Signature 531 | 532 | -spec hmac_sha1_signature(HttpMethod, URL, Params, Consumer, TokenSecret) -> Result when 533 | HttpMethod :: string(), 534 | URL :: httpc:url(), 535 | Params :: [{ParamsKey, ParamsValue}], 536 | ParamsKey :: Term, 537 | ParamsValue :: Term, 538 | Consumer :: {Key, Secret, Method}, 539 | Key :: string(), 540 | Secret :: string(), 541 | Method :: signature_method(), 542 | TokenSecret :: string(), 543 | Result :: base64:base64_string(). 544 | hmac_sha1_signature(HttpMethod, URL, Params, Consumer, TokenSecret) -> 545 | BaseString = signature_base_string(HttpMethod, URL, Params), 546 | hmac_sha1_signature(BaseString, Consumer, TokenSecret). 547 | 548 | %% @doc Build HMAC-SHA1 Signature 549 | 550 | -spec hmac_sha1_signature(BaseString, Consumer, TokenSecret) -> Result when 551 | BaseString :: string() | binary(), 552 | Consumer :: {Key, Secret, Method}, 553 | Key :: string(), 554 | Secret :: string(), 555 | Method :: signature_method(), 556 | TokenSecret :: string(), 557 | Result :: base64:base64_string(). 558 | hmac_sha1_signature(BaseString, Consumer, TokenSecret) -> 559 | Key = uri_join([consumer_secret(Consumer), TokenSecret]), 560 | base64:encode_to_string(?HMAC_SHA1(Key, BaseString)). 561 | 562 | %% @doc Verify HMAC-SHA1 Signature 563 | 564 | -spec hmac_sha1_verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) -> Result when 565 | Signature :: base64:base64_string() | base64:base64_binary(), 566 | HttpMethod :: string(), 567 | URL :: httpc:url(), 568 | Params :: [{ParamsKey, ParamsValue}], 569 | ParamsKey :: Term, 570 | ParamsValue :: Term, 571 | Consumer :: {Key, Secret, Method}, 572 | Key :: string(), 573 | Secret :: string(), 574 | Method :: signature_method(), 575 | TokenSecret :: string(), 576 | Result :: boolean(). 577 | hmac_sha1_verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) -> 578 | verify_in_constant_time(Signature, hmac_sha1_signature(HttpMethod, URL, Params, Consumer, TokenSecret)). 579 | 580 | %% @doc Verify HMAC-SHA1 Signature 581 | 582 | -spec hmac_sha1_verify(Signature, BaseString, Consumer, TokenSecret) -> Result when 583 | Signature :: base64:base64_string() | base64:base64_binary(), 584 | BaseString :: string() | binary(), 585 | Consumer :: {Key, Secret, Method}, 586 | Key :: string(), 587 | Secret :: string(), 588 | Method :: signature_method(), 589 | TokenSecret :: string(), 590 | Result :: boolean(). 591 | hmac_sha1_verify(Signature, BaseString, Consumer, TokenSecret) -> 592 | verify_in_constant_time(Signature, hmac_sha1_signature(BaseString, Consumer, TokenSecret)). 593 | 594 | %%@doc Build RSA_SHA1 Signature 595 | 596 | -spec rsa_sha1_signature(HttpMethod, URL, Params, Consumer) -> Result when 597 | HttpMethod :: string(), 598 | URL :: httpc:url(), 599 | Params :: [{ParamsKey, ParamsValue}], 600 | ParamsKey :: Term, 601 | ParamsValue :: Term, 602 | Consumer :: {Key, Secret, Method}, 603 | Key :: string(), 604 | Secret :: string(), 605 | Method :: signature_method(), 606 | Result :: base64:base64_string(). 607 | rsa_sha1_signature(HttpMethod, URL, Params, Consumer) -> 608 | BaseString = signature_base_string(HttpMethod, URL, Params), 609 | rsa_sha1_signature(BaseString, Consumer). 610 | 611 | %%@doc Build RSA_SHA1 Signature 612 | 613 | -spec rsa_sha1_signature(BaseString, Consumer) -> Result when 614 | BaseString :: string() | binary(), 615 | Consumer :: {Key, Secret, Method}, 616 | Key :: string(), 617 | Secret :: string(), 618 | Method :: signature_method(), 619 | Result :: base64:base64_string(). 620 | rsa_sha1_signature(BaseString, Consumer) -> 621 | Key = read_private_key(consumer_secret(Consumer)), 622 | base64:encode_to_string(public_key:sign(list_to_binary(BaseString), sha, Key)). 623 | 624 | %%@doc Verify RSA_SHA1 Signature 625 | 626 | -spec rsa_sha1_verify(Signature, HttpMethod, URL, Params, Consumer) -> Result when 627 | Signature :: base64:base64_string() | base64:base64_binary(), 628 | HttpMethod :: string(), 629 | URL :: httpc:url(), 630 | Params :: [{ParamsKey, ParamsValue}], 631 | ParamsKey :: Term, 632 | ParamsValue :: Term, 633 | Consumer :: {Key, Secret, Method}, 634 | Key :: string(), 635 | Secret :: string(), 636 | Method :: signature_method(), 637 | Result :: boolean(). 638 | rsa_sha1_verify(Signature, HttpMethod, URL, Params, Consumer) -> 639 | BaseString = signature_base_string(HttpMethod, URL, Params), 640 | rsa_sha1_verify(Signature, BaseString, Consumer). 641 | 642 | %%@doc Verify RSA_SHA1 Signature 643 | 644 | -spec rsa_sha1_verify(Signature, BaseString, Consumer) -> Result when 645 | Signature :: base64:base64_string() | base64:base64_binary(), 646 | BaseString :: string() | binary(), 647 | Consumer :: {Key, Secret, Method}, 648 | Key :: string(), 649 | Secret :: string(), 650 | Method :: signature_method(), 651 | Result :: boolean(). 652 | rsa_sha1_verify(Signature, BaseString, Consumer) when is_binary(BaseString) -> 653 | Key = read_cert_key(consumer_secret(Consumer)), 654 | public_key:verify(BaseString, sha, base64:decode(Signature), Key); 655 | rsa_sha1_verify(Signature, BaseString, Consumer) when is_list(BaseString) -> 656 | rsa_sha1_verify(Signature, list_to_binary(BaseString), Consumer). 657 | 658 | -spec verify_in_constant_time(X,Y) -> Result when 659 | X :: list(), 660 | Y :: list(), 661 | Result :: boolean(). 662 | verify_in_constant_time(X, Y) when is_list(X) and is_list(Y) -> 663 | case length(X) == length(Y) of 664 | true -> 665 | verify_in_constant_time(X, Y, 0); 666 | false -> 667 | false 668 | end. 669 | 670 | -spec verify_in_constant_time(X, Y, Result) -> FunResult when 671 | X :: list(), 672 | Y :: list(), 673 | Result :: integer(), 674 | FunResult :: boolean(). 675 | verify_in_constant_time([X | RestX], [Y | RestY], Result) -> 676 | verify_in_constant_time(RestX, RestY, (X bxor Y) bor Result); 677 | verify_in_constant_time([], [], Result) -> 678 | Result == 0. 679 | 680 | %% @doc Build signature string. 681 | -spec signature_base_string(HttpMethod, URL, Params) -> Result when 682 | HttpMethod :: string(), 683 | URL :: uri_string:uri_string(), 684 | Params :: list(), 685 | Result :: uri_string:uri_string(). 686 | signature_base_string(HttpMethod, URL, Params) -> 687 | uri_join([HttpMethod, base_string_uri(URL), params_encode(Params)]). 688 | 689 | %%@doc Encode provided parameters. 690 | 691 | -spec params_encode(Params) -> Result when 692 | Params :: [{Key, Value}], 693 | Key :: Term, 694 | Value :: Term, 695 | Term :: integer() | atom() | binary() | list(), 696 | Result :: string(). 697 | params_encode(Params) -> 698 | % cf. http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 699 | Encoded = [{uri_encode(K), uri_encode(V)} || {K, V} <- Params], 700 | Sorted = lists:sort(Encoded), 701 | Concatenated = [lists:concat([K, "=", V]) || {K, V} <- Sorted], 702 | string:join(Concatenated, "&"). 703 | 704 | %%@doc Decode provided parameters from query string. 705 | 706 | -spec params_decode(Response) -> Result when 707 | Response :: {{term(), term(), term()}, term(), Body}, 708 | Body :: QueryString, 709 | QueryString :: uri_string:uri_string(), 710 | Result :: QueryList, 711 | QueryList :: [{unicode:chardata(), unicode:chardata() | true}] | uri_string:error(). 712 | params_decode(_Response={{_, _, _}, _, Body}) -> 713 | uri_string:dissect_query(Body). 714 | 715 | -spec http_request(Method, Request, Options) -> Result when 716 | Method :: httpc:method(), 717 | Request :: httpc:request(), 718 | Options :: [Option], 719 | Option :: {sync, boolean()} | {stream, StreamTo} | {body_format, BodyFormat} | 720 | {full_result, boolean()} | {headers_as_is, boolean()} | {socket_opts, SocketOpts} | 721 | {receiver, Receiver}, 722 | StreamTo :: none | self | {self, once} | httpc:filename(), 723 | BodyFormat :: 'string' | 'binary', 724 | SocketOpts :: [httpc:socket_opt()], 725 | Receiver :: Receiver :: pid() | function() | {Module, Function, Args}, 726 | Module :: atom(), 727 | Function :: atom(), 728 | Args :: list(), 729 | Result :: {ok, OkResult} | {ok, saved_to_file} | {error, Reason}, 730 | OkResult :: {httpc:status_line(), httpc:headers(), Body} | {httpc:status_code(), Body} | httpc:request_id(), 731 | Body :: httpc:http_string() | binary(), 732 | Reason :: term(). 733 | http_request(Method, Request, Options) -> 734 | httpc:request(Method, Request, [{autoredirect, false}], Options). 735 | 736 | -define(unix_epoch, 62167219200). 737 | 738 | -spec unix_timestamp() -> Result when 739 | Result :: non_neg_integer(). 740 | unix_timestamp() -> 741 | calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - ?unix_epoch. 742 | 743 | read_cert_key(Path) when is_list(Path) -> 744 | {ok, Contents} = file:read_file(Path), 745 | [{'Certificate', DerCert, not_encrypted}] = public_key:pem_decode(Contents), 746 | read_cert_key(public_key:pkix_decode_cert(DerCert, otp)); 747 | read_cert_key(#'OTPCertificate'{tbsCertificate=Cert}) -> 748 | read_cert_key(Cert); 749 | read_cert_key(#'OTPTBSCertificate'{subjectPublicKeyInfo=Info}) -> 750 | read_cert_key(Info); 751 | read_cert_key(#'OTPSubjectPublicKeyInfo'{subjectPublicKey=Key}) -> 752 | Key. 753 | 754 | -spec read_private_key(Path) -> Result when 755 | Path :: file:name_all(), 756 | Result :: term(). 757 | read_private_key(Path) -> 758 | {ok, Contents} = file:read_file(Path), 759 | [Info] = public_key:pem_decode(Contents), 760 | public_key:pem_entry_decode(Info). 761 | 762 | %% @doc Encode authorization paramaters list as a string. 763 | %% @returns Encoded authorization paramaters list as a string. 764 | -spec header_params_encode(Params) -> Result when 765 | Params :: [{Key, Value}], 766 | Key :: Term, 767 | Value :: Term, 768 | Term :: integer() | atom() | binary() | list(), 769 | Result :: string(). 770 | header_params_encode(Params) -> 771 | intercalate(", ", [lists:concat([uri_encode(K), "=\"", uri_encode(V), "\""]) || {K, V} <- Params]). 772 | 773 | %% @doc Dencode authorization paramaters list. 774 | %% @returns Dencoded authorization paramaters list. 775 | -spec header_params_decode(String) -> Result when 776 | String :: string(), 777 | Result :: [Param], 778 | Param :: {Key, Value}, 779 | Key :: string(), 780 | Value :: string(). 781 | header_params_decode(String) -> 782 | [header_param_decode(Param) || Param <- re:split(String, ",\\s*", [{return, list}]), Param =/= ""]. 783 | 784 | -spec header_param_decode(Param) -> Result when 785 | Param :: SeparatorList, 786 | SeparatorList :: string(), 787 | Result :: {Key, Value}, 788 | Key :: string(), 789 | Value :: string(). 790 | header_param_decode(Param) -> 791 | [Key, QuotedValue] = string:tokens(Param, "="), 792 | Value = string:substr(QuotedValue, 2, length(QuotedValue) - 2), 793 | {uri_decode(Key), uri_decode(Value)}. 794 | 795 | -spec base_string_uri(Str) -> Result when 796 | Str :: URIString, 797 | URIString :: uri_string:uri_string(), 798 | Result :: URIString, 799 | URIString :: uri_string:uri_string() | uri_string:error(). 800 | base_string_uri(Str) -> 801 | % https://tools.ietf.org/html/rfc5849#section-3.4.1.2 802 | Map1 = uri_string:parse(Str), 803 | Scheme = string:to_lower(maps:get(scheme, Map1)), 804 | Host = string:to_lower(maps:get(host, Map1)), 805 | Map2 = maps:put(scheme, Scheme, Map1), 806 | Map3 = maps:put(host, Host, Map2), 807 | Map4 = maps:remove(query, Map3), 808 | Map5 = without_default_port(Scheme, Map4), 809 | uri_string:recompose(Map5). 810 | 811 | without_default_port("http", #{ port := 80 } = Map) -> 812 | maps:remove(port, Map); 813 | without_default_port("https", #{ port := 443 } = Map) -> 814 | maps:remove(port, Map); 815 | without_default_port(_Scheme, Map) -> 816 | Map. 817 | 818 | %% @equiv uri_join(Values, "&") 819 | 820 | -spec uri_join(Values) -> Result when 821 | Values :: list(integer() | atom() | binary() | list()), 822 | Result :: string(). 823 | uri_join(Values) -> 824 | uri_join(Values, "&"). 825 | 826 | -spec uri_join(Values, Separator) -> Result when 827 | Values :: list(integer() | atom() | binary() | list()), 828 | Separator :: string(), 829 | Result :: string(). 830 | uri_join(Values, Separator) -> 831 | string:join(lists:map(fun uri_encode/1, Values), Separator). 832 | 833 | intercalate(Sep, Xs) -> 834 | lists:concat(intersperse(Sep, Xs)). 835 | 836 | intersperse(_, []) -> 837 | []; 838 | intersperse(_, [X]) -> 839 | [X]; 840 | intersperse(Sep, [X | Xs]) -> 841 | [X, Sep | intersperse(Sep, Xs)]. 842 | 843 | -spec uri_encode(Term) -> Result when 844 | Term :: integer() | atom() | binary() | list(), 845 | Result :: string(). 846 | uri_encode(Term) when is_integer(Term) -> 847 | integer_to_list(Term); 848 | uri_encode(Term) when is_atom(Term) -> 849 | uri_encode(atom_to_list(Term)); 850 | uri_encode(Term) when is_binary(Term) -> 851 | uri_encode(binary_to_list(Term)); 852 | uri_encode(Term) when is_list(Term) -> 853 | uri_encode(lists:reverse(Term, []), []). 854 | 855 | -define(is_alphanum(C), C >= $A, C =< $Z; C >= $a, C =< $z; C >= $0, C =< $9). 856 | 857 | -spec uri_encode(Term, Acc) -> Result when 858 | Term :: string(), 859 | Acc :: list(), 860 | Result :: string(). 861 | uri_encode([X | T], Acc) when ?is_alphanum(X); X =:= $-; X =:= $_; X =:= $.; X =:= $~ -> 862 | uri_encode(T, [X | Acc]); 863 | uri_encode([X | T], Acc) -> 864 | NewAcc = [$%, dec2hex(X bsr 4), dec2hex(X band 16#0f) | Acc], 865 | uri_encode(T, NewAcc); 866 | uri_encode([], Acc) -> 867 | Acc. 868 | 869 | %% @equiv uri_decode(Str, []) 870 | 871 | -spec uri_decode(Str) -> Result when 872 | Str :: string(), 873 | Result :: string(). 874 | uri_decode(Str) when is_list(Str) -> 875 | uri_decode(Str, []). 876 | 877 | uri_decode([$%, A, B | T], Acc) -> 878 | uri_decode(T, [(hex2dec(A) bsl 4) + hex2dec(B) | Acc]); 879 | uri_decode([X | T], Acc) -> 880 | uri_decode(T, [X | Acc]); 881 | uri_decode([], Acc) -> 882 | lists:reverse(Acc, []). 883 | 884 | -compile({inline, [{dec2hex, 1}, {hex2dec, 1}]}). 885 | 886 | dec2hex(N) when N >= 10 andalso N =< 15 -> 887 | N + $A - 10; 888 | dec2hex(N) when N >= 0 andalso N =< 9 -> 889 | N + $0. 890 | 891 | hex2dec(C) when C >= $A andalso C =< $F -> 892 | C - $A + 10; 893 | hex2dec(C) when C >= $0 andalso C =< $9 -> 894 | C - $0. 895 | --------------------------------------------------------------------------------