diff options
Diffstat (limited to 'plugins/common/OAuth1Support.vala')
-rw-r--r-- | plugins/common/OAuth1Support.vala | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/plugins/common/OAuth1Support.vala b/plugins/common/OAuth1Support.vala new file mode 100644 index 0000000..e5a8545 --- /dev/null +++ b/plugins/common/OAuth1Support.vala @@ -0,0 +1,233 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2017 Jens Georg <mail@jensge.org> + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Publishing.RESTSupport.OAuth1 { + internal const string ENCODE_RFC_3986_EXTRA = "!*'();:@&=+$,/?%#[] \\"; + + public class Session : Publishing.RESTSupport.Session { + private string? request_phase_token = null; + private string? request_phase_token_secret = null; + private string? access_phase_token = null; + private string? access_phase_token_secret = null; + private string? username = null; + private string? consumer_key = null; + private string? consumer_secret = null; + + public Session(string? endpoint_uri = null) { + base(endpoint_uri); + } + + public override bool is_authenticated() { + return (access_phase_token != null && access_phase_token_secret != null && + username != null); + } + + public void authenticate_from_persistent_credentials(string token, string secret, + string username) { + this.access_phase_token = token; + this.access_phase_token_secret = secret; + this.username = username; + + this.authenticated(); + } + + public void deauthenticate() { + access_phase_token = null; + access_phase_token_secret = null; + username = null; + } + + public void set_api_credentials(string consumer_key, string consumer_secret) { + this.consumer_key = consumer_key; + this.consumer_secret = consumer_secret; + } + + public string sign_transaction(Publishing.RESTSupport.Transaction txn, + Publishing.RESTSupport.Argument[]? extra_arguments = null) { + string http_method = txn.get_method().to_string(); + + debug("signing transaction with parameters:"); + debug("HTTP method = " + http_method); + + Publishing.RESTSupport.Argument[] base_string_arguments = txn.get_arguments(); + + foreach (var arg in extra_arguments) { + base_string_arguments += arg; + } + + Publishing.RESTSupport.Argument[] sorted_args = + Publishing.RESTSupport.Argument.sort(base_string_arguments); + + var arguments_string = Argument.serialize_list(sorted_args); + + string? signing_key = null; + if (access_phase_token_secret != null) { + debug("access phase token secret available; using it as signing key"); + + signing_key = consumer_secret + "&" + access_phase_token_secret; + } else if (request_phase_token_secret != null) { + debug("request phase token secret available; using it as signing key"); + + signing_key = consumer_secret + "&" + request_phase_token_secret; + } else { + debug("neither access phase nor request phase token secrets available; using API " + + "key as signing key"); + + signing_key = consumer_secret + "&"; + } + + string signature_base_string = http_method + "&" + Soup.URI.encode( + txn.get_endpoint_url(), ENCODE_RFC_3986_EXTRA) + "&" + + Soup.URI.encode(arguments_string, ENCODE_RFC_3986_EXTRA); + + debug("signature base string = '%s'", signature_base_string); + + debug("signing key = '%s'", signing_key); + + // compute the signature + string signature = RESTSupport.hmac_sha1(signing_key, signature_base_string); + signature = Soup.URI.encode(signature, ENCODE_RFC_3986_EXTRA); + + debug("signature = '%s'", signature); + + return signature; + } + + public void set_request_phase_credentials(string token, string secret) { + this.request_phase_token = token; + this.request_phase_token_secret = secret; + } + + public void set_access_phase_credentials(string token, string secret, string username) { + this.access_phase_token = token; + this.access_phase_token_secret = secret; + this.username = username; + + authenticated(); + } + + public string get_oauth_nonce() { + TimeVal currtime = TimeVal(); + currtime.get_current_time(); + + return Checksum.compute_for_string(ChecksumType.MD5, currtime.tv_sec.to_string() + + currtime.tv_usec.to_string()); + } + + public string get_oauth_timestamp() { + return GLib.get_real_time().to_string().substring(0, 10); + } + + public string get_consumer_key() { + assert(consumer_key != null); + return consumer_key; + } + + public string get_request_phase_token() { + assert(request_phase_token != null); + return request_phase_token; + } + + public string get_access_phase_token() { + assert(access_phase_token != null); + return access_phase_token; + } + + public bool has_access_phase_token() { + return access_phase_token != null; + } + + public string get_access_phase_token_secret() { + assert(access_phase_token_secret != null); + return access_phase_token_secret; + } + + public string get_username() { + assert(is_authenticated()); + return username; + } + } + + public class Transaction : Publishing.RESTSupport.Transaction { + public Transaction(Session session, Publishing.RESTSupport.HttpMethod method = + Publishing.RESTSupport.HttpMethod.POST) { + base(session, method); + setup_arguments(); + } + + public Transaction.with_uri(Session session, string uri, + Publishing.RESTSupport.HttpMethod method = Publishing.RESTSupport.HttpMethod.POST) { + base.with_endpoint_url(session, uri, method); + setup_arguments(); + } + + private void setup_arguments() { + var session = (Session) get_parent_session(); + + add_argument("oauth_nonce", session.get_oauth_nonce()); + add_argument("oauth_signature_method", "HMAC-SHA1"); + add_argument("oauth_version", "1.0"); + add_argument("oauth_timestamp", session.get_oauth_timestamp()); + add_argument("oauth_consumer_key", session.get_consumer_key()); + if (session.has_access_phase_token()) { + add_argument("oauth_token", session.get_access_phase_token()); + } + } + + + public override void execute() throws Spit.Publishing.PublishingError { + var signature = ((Session) get_parent_session()).sign_transaction(this); + add_argument("oauth_signature", signature); + + base.execute(); + } + } + + public class UploadTransaction : Publishing.RESTSupport.UploadTransaction { + protected unowned Publishing.RESTSupport.OAuth1.Session session; + private Publishing.RESTSupport.Argument[] auth_header_fields; + + public UploadTransaction(Publishing.RESTSupport.OAuth1.Session session, + Spit.Publishing.Publishable publishable, + string endpoint_uri) { + base.with_endpoint_url(session, publishable, endpoint_uri); + + this.auth_header_fields = new Publishing.RESTSupport.Argument[0]; + this.session = session; + + add_authorization_header_field("oauth_nonce", session.get_oauth_nonce()); + add_authorization_header_field("oauth_signature_method", "HMAC-SHA1"); + add_authorization_header_field("oauth_version", "1.0"); + add_authorization_header_field("oauth_timestamp", session.get_oauth_timestamp()); + add_authorization_header_field("oauth_consumer_key", session.get_consumer_key()); + add_authorization_header_field("oauth_token", session.get_access_phase_token()); + } + + public void add_authorization_header_field(string key, string value) { + auth_header_fields += new Publishing.RESTSupport.Argument(key, value); + } + + public string get_authorization_header_string() { + return "OAuth " + Argument.serialize_list(auth_header_fields, true, ", "); + } + + public void authorize() { + var signature = session.sign_transaction(this, auth_header_fields); + add_authorization_header_field("oauth_signature", signature); + + + string authorization_header = get_authorization_header_string(); + + debug("executing upload transaction: authorization header string = '%s'", + authorization_header); + add_header("Authorization", authorization_header); + + } + } +} + + |