def store_token(self, data): Core.debug("storing OAuth token for %r", self.domain) self._store_token_libsecret(data) try: self._store_token_file(data) except Exception as e: Core.warn("could not write %r: %r", self.token_path, e)
def _forget_token(self): Core.debug("flushing OAuth tokens") nullroute.sec.clear_libsecret({ "xdg:schema": self.TOKEN_SCHEMA, "domain": "pixiv.net" }) os.unlink(self.TOKEN_PATH)
def get_illust_info(self, illust_id): Core.trace("calling api.works(illust_id=%r)", illust_id) resp = self.api.works(illust_id) if resp["status"] == "success": return resp["response"][0] else: raise PixivApiError("API call failed: %r" % resp)
def get_member_info(self, member_id): Core.trace("calling api.users(member_id=%r)", member_id) resp = self.api.users(member_id) if resp["status"] == "success": return resp["response"][0] else: raise PixivApiError("API call failed: %r" % resp)
def increment_attr(self, dn, attr, incr=1, use_increment=True): import random import time if use_increment and \ self.has_control(OID_LDAP_CONTROL_POSTREAD) and \ self.has_feature(OID_LDAP_FEATURE_MODIFY_INCREMENT): incr = str(incr).encode() ctrl = ldap.controls.readentry.PostReadControl(attrList=[attr]) res = self.conn.modify_ext_s(dn, [(ldap.MOD_INCREMENT, attr, incr)], serverctrls=[ctrl]) for outctrl in res[3]: if outctrl.controlType == ctrl.controlType: values = CaseInsensitiveDict(outctrl.entry)[attr] return int(values[0]) wait = 0 while True: old_val = self.read_attr(dn, attr, raw=True)[0] new_val = str(int(old_val) + incr).encode() try: self.conn.modify_s(dn, [(ldap.MOD_DELETE, attr, old_val), (ldap.MOD_ADD, attr, new_val)]) done = True except ldap.NO_SUCH_ATTRIBUTE as e: Core.debug("swap (%r, %r) failed: %r", old_val, new_val, e) wait += 1 time.sleep(0.05 * 2**random.randint(0, wait)) else: break return int(new_val)
def _get_json(self, *args, **kwargs): resp = self.get(*args, **kwargs) resp.raise_for_status() data = json.loads(resp.text, object_hook=ObjectDict) Core.trace("JSON (%r) = %r", args, data) if data.get("error"): raise Exception("API error: %r", data.get("message") or data["error"]) else: return data["body"]
def clear_libsecret(attributes): Core.trace("libsecret clear: %r", attributes) cmd = ["secret-tool", "clear"] for k, v in attributes.items(): cmd += [str(k), str(v)] r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if r.returncode != 0: raise KeyError("libsecret clear failed: %r" % r.stderr.decode())
def __init__(self): self.ua = requests.Session() self.ua.mount("http://", requests.adapters.HTTPAdapter(max_retries=3)) self.ua.mount("https://", requests.adapters.HTTPAdapter(max_retries=3)) self.api = pixivpy3.PixivAPI() if not hasattr(self.api, "client_secret"): Core.warn("this pixivpy3.PixivAPI version does not allow overridding client_secret; OAuth won't work properly") self.api.client_id = "MOBrBDS8blbauoSck0ZfDbtuzpyT" self.api.client_secret = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj" self.api.requests = self.ua
def get_libsecret(attributes): Core.trace("libsecret query: %r", attributes) cmd = ["secret-tool", "lookup"] for k, v in attributes.items(): cmd += [str(k), str(v)] r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if r.returncode != 0: raise KeyError("libsecret lookup failed: %r" % r.stderr.decode()) return r.stdout.decode()
def _discover_endpoints(self): if not self.discovery_url: raise ValueError( "either discovery URL or endpoint URLs must be specified") Core.debug("fetching discovery document %r", self.discovery_url) response = urllib.request.urlopen(self.discovery_url).read() response = json.loads(response) Core.debug("response data: %r", response) if not self.authorization_url: self.authorization_url = response["authorization_endpoint"] if not self.token_grant_url: self.token_grant_url = response["token_endpoint"]
def load_token(self): Core.debug("loading OAuth token for %r", self.domain) try: return self._load_token_libsecret() except KeyError: try: return self._load_token_file() except FileNotFoundError: pass except Exception as e: Core.debug("could not load %r: %r", self.token_path, e) self.forget_token() return None
def _load_member_name_map(self): map_path = Env.find_config_file("pixiv_member_names.txt") try: Core.debug("loading member aliases from %r", map_path) with open(map_path, "r") as fh: self.member_name_map = {} for line in fh: if line.startswith((";", "#", "\n")): continue k, v = line.split("=") self.member_name_map[k.strip()] = v.strip() except FileNotFoundError: Core.debug("member alias file %r not found; ignoring", map_path)
def store_libsecret(label, secret, attributes): Core.trace("libsecret store: %r %r", label, attributes) cmd = ["secret-tool", "store", "--label=%s" % label] for k, v in attributes.items(): cmd += [str(k), str(v)] r = subprocess.run(cmd, input=secret.encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) if r.returncode != 0: raise IOError("libsecret store failed: (%r, %r)" % (r.returncode, r.stderr.decode()))
def __init__(self, url, require_tls=True): Core.debug("creating libldap connection to %r", url) self.conn = ldap.initialize(url) if require_tls and not url.startswith(("ldaps://", "ldapi://")): self.conn.start_tls_s() self.rootDSE = CaseInsensitiveDict(self.conn.read_rootdse_s()) self._controls = { v.decode() for v in self.rootDSE.get("supportedControl", []) } self._features = { v.decode() for v in self.rootDSE.get("supportedFeatures", []) }
def request_tls_certificate(self, domains, csr, years=3): Core.debug("posting a ssl_multi_domain order for %r", domains) org = self.get_organizations()[0] order = { "certificate": { "common_name": domains[0], "csr": csr, "dns_names": domains, "signature_hash": "sha256", }, "organization": { "id": org["id"] }, "validity_years": years, } data = self.post_order("ssl_multi_domain", order) return data
def _load_token_libsecret(self): if self.user_name: try: data = nullroute.sec.get_libsecret(self.match_fields) return json.loads(data) except KeyError: Core.debug("entry not found; retrying without username") old_match_fields = {**self.match_fields} del old_match_fields["username"] data = nullroute.sec.get_libsecret(old_match_fields) Core.debug("migrating entry to add username field") nullroute.sec.clear_libsecret(old_match_fields) self._store_token_libsecret(json.loads(data)) return json.loads(data) else: data = nullroute.sec.get_libsecret(self.match_fields) return json.loads(data)
def _validate(self): Core.debug("verifying session status") resp = self.get("https://www.pixiv.net/member.php", allow_redirects=False) if resp.is_redirect: Core.trace("member.php redirects to %r", resp.next.url) url = requests.utils.urlparse(resp.next.url) if url.path == "/member.php": query = parse_query_string(url.query) self.user_id = int(query["id"]) Core.debug("session is valid, userid %r", self.user_id) return True Core.debug("session is not valid") return False
def _store_token(self, token): data = { "access_token": token.response.access_token, "refresh_token": token.response.refresh_token, "expires_at": int(time.time() + token.response.expires_in), "user_id": token.response.user.id, } Core.debug("storing OAuth tokens") nullroute.sec.store_libsecret("Pixiv OAuth token", json.dumps(data), {"xdg:schema": self.TOKEN_SCHEMA, "domain": "pixiv.net", "userid": token.response.user.id, "username": token.response.user.account}) try: with open(self.TOKEN_PATH, "w") as fh: json.dump(data, fh) return True except Exception as e: Core.warn("could not write %r: %r", self.TOKEN_PATH, e) return False
def end_rename(self, new_filename): if "/" in new_filename: raise ValueError("end_rename() expects only basename, not full path") old_path = self.old_path new_path = os.path.join(os.path.dirname(old_path), new_filename) old_filename = os.path.basename(old_path) if old_filename == new_filename: print("=>", self.fmt_same % "[no change]") elif compare_files(old_path, new_path): print("=>", self.fmt_same % new_filename, "[same]") if not self.dry_run: #os.unlink(old_path) gio_trash_file(old_path) elif os.path.exists(new_path): print("=>", self.fmt_notfound % new_filename, "[diff]") Core.err("refusing to overwrite existing file %r", new_filename) else: print("=>", self.fmt_found % new_filename) if not self.dry_run: #os.rename(old_path, new_path) gio_rename_file(old_path, new_path)
def _authenticate(self): if self.user_id: return True psid = os.environ.get("PIXIV_PHPSESSID") if psid: cookie = requests.cookies.create_cookie(name="PHPSESSID", value=psid, domain=".pixiv.net") cookie.expires = int(time.time() + 3600) Core.debug("storing cookie: %r", cookie) self._store_token(serialize_cookie(cookie)) token = self._load_token() if token: if os.environ.get("FORCE_TOKEN_REFRESH"): del os.environ["FORCE_TOKEN_REFRESH"] token_valid = False else: #token_valid = token["expires"] >= time.time() # Just pretend the cookie is still valid, as it now comes # from the web browser which will keep it active, and we # don't really have any way to get a new one anyway. token_valid = True token["expires"] = int(time.time() + 86400 * 30) if token_valid: cookie = requests.cookies.create_cookie(**token) Core.debug("loaded cookie: %r", cookie) self.ua.cookies.set_cookie(cookie) if self._validate(): cookie.expires = int(time.time() + 86400 * 30) Core.debug("updating cookie: %r", cookie) self._store_token(serialize_cookie(cookie)) return True else: Core.debug("cookie has expired") raise Exception("Pixiv cookie not found or expired")
def _grant_token(self, params): if not self.token_grant_url: self._discover_endpoints() Core.debug("token grant URL: %r", self.token_grant_url) post_data = { "client_id": self.client_id, "client_secret": self.client_secret, **params } Core.debug("request data: %r", post_data) post_data = urllib.parse.urlencode(post_data).encode() Core.debug("encoded data: %r", post_data) response = urllib.request.urlopen(self.token_grant_url, post_data).read() response = json.loads(response) response.setdefault("expires_at", int(time.time() + response["expires_in"])) return response
def _load_token(self): try: data = nullroute.sec.get_libsecret({"xdg:schema": self.TOKEN_SCHEMA, "domain": "pixiv.net"}) Core.debug("found OAuth token in keyring") return json.loads(data) except KeyError: try: with open(self.TOKEN_PATH, "r") as fh: data = json.load(fh) Core.debug("found OAuth token in filesystem") return data except FileNotFoundError: pass except Exception as e: Core.debug("could not load %r: %r", self.TOKEN_PATH, e) self._forget_token() return None
def forget_token(self): Core.debug("flushing OAuth tokens for %r", self.domain) self._clear_token_libsecret() self._clear_token_file()
def post(self, ep, *args, **kwargs): uri = self.base + ep Core.debug("posting to %r" % uri) resp = self.ua.post(uri, *args, **kwargs) resp.raise_for_status() return resp
def get(self, ep, *args, **kwargs): uri = self.base + ep Core.debug("fetching %r" % uri) resp = self.ua.get(uri, *args, **kwargs) resp.raise_for_status() return resp
def _authenticate(self): if self.api.user_id: Core.warn("BUG: _authenticate() called twice") return True data = self._load_token() if data: Core.trace("loaded token: %r", data) if os.environ.get("FORCE_TOKEN_REFRESH"): token_valid = False else: token_valid = data["expires_at"] > time.time() if token_valid: Core.debug("access token within expiry time, using as-is") self.api.user_id = data["user_id"] self.api.access_token = data["access_token"] self.api.refresh_token = data["refresh_token"] return True else: Core.debug("access token has expired, renewing") try: token = self.api.auth(refresh_token=data["refresh_token"]) Core.trace("retrieved token: %r", token) except Exception as e: Core.warn("could not refresh access token: %r", e) #self._forget_token() else: self._store_token(token) return True data = self._load_creds() if data: Core.info("logging in to Pixiv as %r", data["login"]) try: token = self.api.auth(username=data["login"], password=data["password"]) except Exception as e: Core.warn("could not log in using username & password: %r", e) else: self._store_token(token) return True Core.die("could not log in to Pixiv (no credentials)") return False
# https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-00 def hash_data(hash_algo, data): if hash_algo in {"sha1", "sha256", "sha512"}: return hashlib.new(hash_algo, data).digest() else: raise UnsupportedHashType(hash_algo) rest = sys.argv[1:] if rest: cmd, *rest = rest else: Core.die("missing command (try 'sign' or 'verify')") if cmd == "sign": _ap = argparse.ArgumentParser() _ap.add_argument( "--fingerprint", help="Key fingerprint in 'MD5:<hex>' or 'SHA256:<b64>' format") _ap.add_argument("--input-hexdata") _ap.add_argument("--input-string") _ap.add_argument("--test-verify", action="store_true") _ap.add_argument("--namespace") args = _ap.parse_args(rest) if not args.fingerprint: Core.die("signing key (--fingerprint) not specified")
def end_fail(self, e): print(self.fmt_notfound % "failed") Core.err(str(e))