1
import requests
import subprocess
import base64


credentials = "login:password"
url = f'urlXXXurl'
body = '{"id": 4986986, "key": "2df534ee-270b-4ab4-83fb-1b308febacce", ...}'


headers = '"""@{Authorization = \'Basic %s\'}"""' % base64.b64encode(credentials.encode("ascii")).decode("ascii")
command = 'powershell.exe Invoke-RestMethod -Method Post -Uri %s -ContentType application/json -Body """%s""" -Headers ( Invoke-Expression %s ) -UseBasicParsing' % (url, body.replace('"', '\"'), headers)
response = subprocess.check_call(command)

Is there some kind of necessary conversion, so that JSON would be recognizable by PowerShell?

Takeren
  • 45
  • 6
  • 2
    You could use [`powershell.exe -EncodedCommand `](https://devblogs.microsoft.com/scripting/powertip-encode-string-and-execute-with-powershell/), then you won't have to worry about double-escaping quotes. But... why not just use the `requests` module for the post request? Why call out to PowerShell in the first place? – Mathias R. Jessen Jul 13 '23 at 14:11
  • 1
    A http request the body is a string (json). No change on need to take you string from pything and send to PS. You do not need to use the replace to change the double quotes. – jdweng Jul 13 '23 at 14:21
  • I have very strange client-server environment where I am not able to call python requests, tried that. I can execute some subprocess.run calls though. And powershell works, I also considered cmd curl. – Takeren Jul 13 '23 at 14:22

2 Answers2

1

Finally I used this approach

command = "powershell.exe ConvertTo-Json ( Invoke-RestMethod -Method Post -Uri %s -ContentType application/json -Body '%s' -Headers ( Invoke-Expression %s ) -UseBasicParsing )" \
                      % (url, json.dumps(self.asset).replace('"', '"""'), headers)
Takeren
  • 45
  • 6
  • 1
    If you want the raw JSON output, then use `Invoke-WebRequest ... |ForEach-Object Content` instead of `ConvertTo-Json (Invoke-RestMethod ...)` - `Invoke-RestMethod` is designed to automatically convert JSON to an object hierarchy, which is obviously not what you want – Mathias R. Jessen Jul 13 '23 at 14:53
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jul 15 '23 at 21:22
1
  • The only immediate problem with the approach in your question was the use of """...""" around the -Body argument (resulting in a "..." string from PowerShell's perspective):

    • Doing so requires two escaping techniques: \ (or """) in order to preserve any " characters as part of the PowerShell command passed to the the (implied) -Command parameter of PowerShell's CLI and `-escaping of the " chars. embedded in the value (...).

    • While you could therefore have used body.replace('"', '`\"') (note the `), it is simpler to use '...' for strings,[1] given that ' does not require escaping on the command line and allows embedding " characters as-is (albeit as \" via the CLI).

  • There is no need for ( Invoke-Expression %s ), given that you can formulate the string representing the -Header argument directly as a PowerShell hashtable literal.

  • As Mathias points out, if you want the command to output a JSON string, use Invoke-WebRequest and access the output object's .Content property.

    • By contrast, if you use Invoke-RestMethod, PowerShell automatically and invariably parses a JSON response into an object graph (which is what necessitated the ConvertTo-Json call in your own answer, which isn't needed if you use (Invoke-WebRequest).Content).

Applying the above to your code, using Python v3.6+ f-strings:

import requests
import subprocess
import base64

credentials = 'login:password'
url = 'urlXXXurl'
body = '{"id": 4986986, "key": "2df534ee-270b-4ab4-83fb-1b308febacce", ...}'

# Construct the header as a string representing a PowerShell hashtable literal.
# Note the the use of {{ and }} to use *literal* { and } chars. in the f-string.
headers = f"@{{Authorization = 'Basic {base64.b64encode(credentials.encode('ascii')).decode('ascii')}'}}"

# Escape the " chars. in JSON as \", 
# so that the PowerShell CLI doesn't strip them.
bodyEscaped = body.replace('"', '\"')

# Note:
#  Due to use of '...' PowerShell strings, the assumption is that
#  neither header nor bodyEscaped themselves contain '
#  If they did, these embedded ' would have to be escaped as ''
command = f"""\
powershell.exe -NoProfile -Command
  (
    Invoke-WebRequest -Uri '{url}' -Body '{bodyEscaped}' -Headers {headers} -Method Post -ContentType application/json -UseBasicParsing
  ).Content
"""

response = subprocess.check_call(command)

Note that neither the -NoProfile nor the -Command parameter of the Windows PowerShell CLI are strictly needed here:

  • -NoProfile bypasses profile (initialization-file) loading and is therefore both potentially faster and makes for a more predictable execution environment.
  • -Command (-c) would be implied if not used, but is included for conceptual clarity (not least because pwsh, the PowerShell (Core) CLI, now requires -Command, as it defaults to -File).

Finally, note that - for simplicity - what constitutes the PowerShell command is technically passed as multiple arguments rather than as a single string enclosed in (unescaped) "...".

  • If (another) shell, notably cmd.exe, were involved in the call (it isn't with subprocess.call()), use of "..." would be advisable, to prevent accidental interpretation of characters such as & and | by that shell.
  • Separately, "..." would also be needed in the rare event that your PowerShell command has embedded strings with runs of multiple spaces that need to be preserved as such (without "..." enclosure, such runs would be folded into a single space each).

[1] This assumes that you don't need PowerShell's string interpolation, which requires "..." strings.

mklement0
  • 382,024
  • 64
  • 607
  • 775