├── .github
└── workflows
│ ├── cicd.yml
│ ├── coverage.yml
│ └── scorecard.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── bourso-launchd.sh
└── src
├── bourso_api
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
│ ├── account.rs
│ ├── client
│ ├── account.rs
│ ├── config.rs
│ ├── error.rs
│ ├── mod.rs
│ ├── trade
│ │ ├── error.rs
│ │ ├── feed.rs
│ │ ├── mod.rs
│ │ ├── order.rs
│ │ └── tick.rs
│ └── virtual_pad.rs
│ ├── constants.rs
│ └── lib.rs
├── lib.rs
├── main.rs
├── settings.rs
└── validate.rs
/.github/workflows/cicd.yml:
--------------------------------------------------------------------------------
1 | # Inspired by https://github.com/Swatinem/rust-cache/blob/master/.github/workflows/simple.yml
2 | name: CI/CD
3 |
4 | on:
5 | push:
6 | tags:
7 | - v*.*.*
8 |
9 | permissions:
10 | contents: write
11 |
12 | env:
13 | CARGO_TERM_COLOR: always
14 |
15 | jobs:
16 | build:
17 | outputs:
18 | hash-ubuntu-latest: ${{ steps.hash.outputs.hash-ubuntu-latest }}
19 | hash-macos-latest: ${{ steps.hash.outputs.hash-macos-latest }}
20 | hash-windows-latest: ${{ steps.hash-windows.outputs.hash-windows-latest }}
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | os: [ubuntu-latest, macos-latest, windows-latest]
25 |
26 | name: Bourso CLI ${{ matrix.os }}
27 | runs-on: ${{ matrix.os }}
28 |
29 | steps:
30 | - uses: actions/checkout@v4
31 |
32 | # When rustup is updated, it tries to replace its binary, which on Windows is somehow locked.
33 | # This can result in the CI failure, see: https://github.com/rust-lang/rustup/issues/3029
34 | - run: |
35 | rustup set auto-self-update disable
36 | rustup toolchain install stable --profile minimal
37 |
38 | - name: Install Rust
39 | uses: dtolnay/rust-toolchain@v1
40 | with:
41 | toolchain: stable
42 | components: rustfmt
43 |
44 | - name: Cache
45 | uses: Swatinem/rust-cache@v2
46 |
47 | - name: Check and test
48 | run: |
49 | cargo check
50 | cargo test
51 | cargo test -p bourso_api
52 |
53 | - name: Build release
54 | run: cargo build --release
55 |
56 | - name: Rename and make executabe Mac Release
57 | if: matrix.os == 'macos-latest'
58 | run: |
59 | chmod +x target/release/bourso-cli
60 | tar -czvf target/release/bourso-cli-darwin.tar.gz -C target/release bourso-cli
61 |
62 | - name: Build Linux Release
63 | if: matrix.os == 'ubuntu-latest'
64 | run: tar -czvf target/release/bourso-cli-linux.tar.gz -C target/release bourso-cli
65 |
66 | - name: Generate subject for Unix-like
67 | if: matrix.os != 'windows-latest'
68 | id: hash
69 | run: |
70 | set -euo pipefail
71 | if [ ${{ matrix.os }} = "ubuntu-latest" ]; then
72 | echo "hash-${{ matrix.os }}=$(sha256sum target/release/bourso-cli | base64 -w0)" >> "$GITHUB_OUTPUT"
73 | elif [ ${{ matrix.os }} = "macos-latest" ]; then
74 | echo "hash-${{ matrix.os }}=$(shasum -a 256 target/release/bourso-cli | base64)" >> "$GITHUB_OUTPUT"
75 | fi
76 |
77 | - name: Generate subject for Windows
78 | if: matrix.os == 'windows-latest'
79 | id: hash-windows
80 | run: |
81 | # Stop script execution on errors (similar to set -e in Unix)
82 | $ErrorActionPreference = "Stop"
83 | $hash = Get-FileHash -Path "target/release/bourso-cli.exe" -Algorithm SHA256
84 | $hashString = $hash.Hash.ToLower() + " target/release/bourso-cli.exe"
85 | $base64Hash = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($hashString))
86 | echo "hash-windows-latest=$base64Hash" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
87 |
88 | - name: Create GitHub Release
89 | uses: softprops/action-gh-release@v2.0.4
90 | if: startsWith(github.ref, 'refs/tags/')
91 | with:
92 | files: |
93 | target/release/bourso-cli-linux.tar.gz
94 | target/release/bourso-cli-darwin.tar.gz
95 | target/release/bourso-cli.exe
96 |
97 | provenance:
98 | needs: [build]
99 | strategy:
100 | matrix:
101 | os: [ubuntu-latest, macos-latest, windows-latest]
102 | permissions:
103 | actions: read # To read the workflow path.
104 | id-token: write # To sign the provenance.
105 | contents: write # To add assets to a release.
106 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
107 | with:
108 | provenance-name: provenance-id-${{ matrix.os }}.intoto.jsonl
109 | base64-subjects: "${{ needs.build.outputs[format('hash-{0}', matrix.os)] }}"
110 | upload-assets: true # Optional: Upload to a new release
111 |
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: coverage
2 |
3 | on: [push]
4 | jobs:
5 | test:
6 | name: coverage
7 | runs-on: ubuntu-latest
8 | container:
9 | image: xd009642/tarpaulin
10 | options: --security-opt seccomp=unconfined
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v2
14 |
15 | - name: Generate code coverage
16 | run: |
17 | cargo tarpaulin --verbose --packages bourso_api --timeout 120 --out xml
18 |
19 | - name: Upload to codecov.io
20 | uses: codecov/codecov-action@v4.3.0
21 | with:
22 | fail_ci_if_error: false
23 | token: ${{ secrets.CODECOV_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/scorecard.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub. They are provided
2 | # by a third-party and are governed by separate terms of service, privacy
3 | # policy, and support documentation.
4 |
5 | name: Scorecard supply-chain security
6 | on:
7 | # For Branch-Protection check. Only the default branch is supported. See
8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
9 | branch_protection_rule:
10 | # To guarantee Maintained check is occasionally updated. See
11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
12 | schedule:
13 | - cron: '21 0 * * 1'
14 | push:
15 | branches: [ "main" ]
16 |
17 | # Declare default permissions as read only.
18 | permissions: read-all
19 |
20 | jobs:
21 | analysis:
22 | name: Scorecard analysis
23 | runs-on: ubuntu-latest
24 | permissions:
25 | # Needed to upload the results to code-scanning dashboard.
26 | security-events: write
27 | # Needed to publish results and get a badge (see publish_results below).
28 | id-token: write
29 | # Uncomment the permissions below if installing in a private repository.
30 | # contents: read
31 | # actions: read
32 |
33 | steps:
34 | - name: "Checkout code"
35 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
36 | with:
37 | persist-credentials: false
38 |
39 | - name: "Run analysis"
40 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
41 | with:
42 | results_file: results.sarif
43 | results_format: sarif
44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
45 | # - you want to enable the Branch-Protection check on a *public* repository, or
46 | # - you are installing Scorecard on a *private* repository
47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }}
49 |
50 | # Public repositories:
51 | # - Publish results to OpenSSF REST API for easy access by consumers
52 | # - Allows the repository to include the Scorecard badge.
53 | # - See https://github.com/ossf/scorecard-action#publishing-results.
54 | # For private repositories:
55 | # - `publish_results` will always be set to `false`, regardless
56 | # of the value entered here.
57 | publish_results: true
58 |
59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
60 | # format to the repository Actions tab.
61 | - name: "Upload artifact"
62 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
63 | with:
64 | name: SARIF file
65 | path: results.sarif
66 | retention-days: 5
67 |
68 | # Upload the results to GitHub's code scanning dashboard.
69 | - name: "Upload to code-scanning"
70 | uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4
71 | with:
72 | sarif_file: results.sarif
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.chls
2 |
3 |
4 | # Added by cargo
5 |
6 | /target
7 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "bourso-cli"
3 | version = "0.2.0"
4 | edition = "2021"
5 | repository = "https://github.com/azerpas/bourso-api"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | bourso_api = { path = "./src/bourso_api" }
11 | tokio = { version = "1.33.0", features = ["full"] }
12 | anyhow = { version = "1.0.75" }
13 | clap = { version = "4.4.6" }
14 | rpassword = { version = "7.2.0" }
15 | directories = { version = "5.0.1" }
16 | serde = { version = "1.0.189", features = ["derive"] }
17 | serde_json = { version = "1.0.107" }
18 | log = { version = "0.4.20" }
19 | log4rs = { version = "1.3.0" }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Azerpas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bourso CLI
2 |
3 | [](https://securityscorecards.dev/viewer/?uri=github.com/azerpas/bourso-api)
4 | [](https://codecov.io/gh/azerpas/bourso-api)
5 |
6 |
7 |
8 |
9 | This app aims to be a simple CLI powered by *[Bourso API](./src/bourso_api/)* to log in to your [BoursoBank/Boursorama](https://www.boursorama.com) account and achieve some basic tasks.
10 |
11 | The first goal of this project was creating an automated [DCA (Dollar Cost Average)](https://www.investopedia.com/terms/d/dollarcostaveraging.asp) solution to buy [ETFs (Exchange Traded Funds)](https://www.investopedia.com/terms/e/etf.asp) on a regular basis with your Bourso account.
12 |
13 | [Follow these instructions](#dca-dollar-cost-averaging-investing) to setup your own automated DCA.
14 |
15 | - [Installation](#installation)
16 | - [From releases](#from-releases)
17 | - [Approve the app (MacOS)](#approve-the-app)
18 | - [Verify your installation](#verify-your-installation)
19 | - [From source](#from-source)
20 | - [Usage](#usage)
21 | - [Get your accounts](#get-your-accounts)
22 | - [Place an order](#place-an-order)
23 | - [Quote 🥷](#quote)
24 | - [DCA](#dca-dollar-cost-averaging-investing)
25 | - [Security](#security)
26 | - [Disclaimer](#disclaimer)
27 |
28 | (🥷 annoted commands require no login)
29 |
30 | ## Installation
31 | ### From releases
32 | You can download the latest release [here](https://github.com/azerpas/bourso-api/releases).
33 |
34 | Choose the right binary for your OS between:
35 | - `bourso-cli-darwin.tar.gz` for MacOS
36 | - `bourso-cli-linux.tar.gz` for Linux
37 | - `bourso-cli.exe` for Windows
38 |
39 | #### Approve the app (MacOS)
40 |
41 | If you then get a `"bourso-cli" cannot be opened because the developer cannot be verified` error, go to `System Preferences > Security & Privacy > General` and click `Open Anyway`
42 |
43 | If the above doesn't help you, make sure the file is executable:
44 | ```sh
45 | chmod +x bourso-cli
46 | # if it still says `Permission denied`, try
47 | chown 777 bourso-cli
48 | ```
49 |
50 | ⚠️ Signing in with a different IP address than the ones you usually use will trigger a security check from Bourso. You'll have to validate the connection from your phone. A [GitHub pull request](https://github.com/azerpas/bourso-api/pull/10) is open to handle this case.
51 |
52 | #### Verify your installation
53 | Bourso CLI embeds [SLSA](https://slsa.dev/) standard to verify the integrity of the binary. You can verify the signature of the binary by:
54 | - Downloading the provenance generated by the release pipeline for your OS: https://github.com/azerpas/bourso-api/releases/latest
55 | - Installing [slsa-verifier](https://github.com/slsa-framework/slsa-verifier?tab=readme-ov-file#installation)
56 | - Running `slsa-verifier`. MacOS example:
57 | ```sh
58 | slsa-verifier-darwin-arm64 verify-artifact --provenance-path provenance-id-macos-latest.intoto.jsonl --source-uri "github.com/azerpas/bourso-api" bourso-cli
59 | ```
60 | Which should output:
61 | ```
62 | Verifying artifact ~/bourso-cli: PASSED
63 | ```
64 |
65 | ### From source
66 | Requires [>=Rust 1.77.2](https://www.rust-lang.org)
67 | ```sh
68 | git clone git@github.com:azerpas/bourso-api.git
69 | cd bourso-api
70 | cargo build --release
71 | # You can run any command from the built application, e.g:
72 | ./target/release/bourso-cli config
73 | ```
74 |
75 | ## Usage
76 |
77 | ### Configuration
78 | Save your client ID with this config command:
79 | ```
80 | ./bourso-cli config
81 | ```
82 | The password will be asked each time you run the app to avoid storing it in a file.
83 |
84 | ### Get your accounts
85 | ```
86 | ./bourso-cli accounts
87 | ```
88 | You'll get something like this:
89 | ```
90 | [
91 | Account {
92 | id: "1a2953bd1a28a37bd3fe89d32986e613",
93 | name: "BoursoBank",
94 | balance: 100,
95 | bank_name: "BoursoBank",
96 | kind: Banking,
97 | },
98 | Account {
99 | id: "a583f3c5842c34fb00b408486ef493e0",
100 | name: "PEA DOE",
101 | balance: 1000000,
102 | bank_name: "BoursoBank",
103 | kind: Trading,
104 | },
105 | ]
106 | ```
107 |
108 | ### Place an order
109 | **Make sure to have a trading account with enough balance to place the order.** Check the previous section to see how to get your account ID.
110 |
111 | 🛍️ Place a buy order for 4 shares of the ETF "1rTCW8" (AMUNDI MSCI WORLD UCITS ETF - EUR) on your account "a583f3c5842c34fb00b408486ef493e0":
112 | ```
113 | ./bourso-cli trade order new --side buy --symbol 1rTCW8 --account a583f3c5842c34fb00b408486ef493e0 --quantity 4
114 | ```
115 |
116 | *Tip: You can get the ETF ID from the tracker URL, e.g. "AMUNDI MSCI WORLD UCITS ETF - EUR" url is https://www.boursorama.com/bourse/trackers/cours/1rTCW8/ (1rTCW8)*
117 |
118 | ### Quote
119 | Quote an asset to retrieve its value over time, e.g:
120 | ```
121 | ➜ ~ ./bourso-cli quote --symbol 1rTCW8 average
122 | INFO bourso_cli > Welcome to BoursoBank CLI 👋
123 | INFO bourso_cli > ℹ️ - Version 0.1.6. Make sure you're running the latest version: https://github.com/azerpas/bourso-api
124 |
125 | INFO bourso_cli > Fetching quotes...
126 | INFO bourso_cli > Average quote: 494.5348136363637
127 | ```
128 | Subcommands available: `highest`, `lowest`, `average`, `volume`, `last`
129 |
130 | ### DCA (Dollar Cost Averaging) investing
131 |
132 | You can use this script to do DCA investing. For example, if you want to buy 1 share of the ETF "1rTCW8" (AMUNDI MSCI WORLD UCITS ETF - EUR) every month, you can use a cron job to run the script every month.
133 |
134 | #### With MacOS
135 | You can use the `launchd` daemon to run the script every week. Create a file named `com.bourso-cli.plist` in `~/Library/LaunchAgents/` with the following content:
136 | ```xml
137 |
138 |
139 |
140 |
141 | Label
142 | com.azerpas.bourso-cli
143 | ProgramArguments
144 |
145 | /Users/YOUR_USER/bourso-launchd.sh
146 |
147 | StartCalendarInterval
148 |
149 | Weekday
150 | 1
151 | Hour
152 | 20
153 | Minute
154 | 00
155 |
156 | RunAtLoad
157 |
158 | StandardOutPath
159 | /Users/YOUR_USER/bourso-launchd-cli-stdout.log
160 | StandardErrorPath
161 | /Users/YOUR_USER/bourso-launchd-cli-stderr.log
162 |
163 |
164 | ```
165 | Replace `YOUR_USER` with your username.
166 |
167 | Then copy the `bourso-launchd.sh` script in your home directory, modify the variables and make it executable.
168 | ```sh
169 | chmod +x bourso-launchd.sh
170 | chown 777 bourso-launchd.sh
171 | ```
172 | Finally, load the agent with the following command:
173 | ```sh
174 | launchctl load ~/Library/LaunchAgents/com.bourso-cli.plist
175 | ```
176 | The script will now run every week at 08:00 PM on Monday. You can check the logs in `~/bourso-launchd-cli-stdout.log` and `~/bourso-launchd-cli-stderr.log`.
177 | #### With Linux
178 | TODO
179 | #### With Windows
180 | Copy/paste the following commands and replace the path with the actual location of `bourso-cli.exe`. Then paste the commands to Powershell.
181 | ```ps1
182 | # Create a new task trigger that will run weekly on Sunday at 1:00pm
183 | $trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 1:00PM
184 | # Create a new task action that will execute the trade based on the trigger defined above
185 | $action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoExit -Command `"& { `$exePath = 'C:\Path\To\bourso-cli.exe'; `$arguments = 'trade order new --side buy --symbol 1rTCW8 --account a583f3c5842c34fb00b408486ef493e0 --quantity 4'; Start-Process -FilePath `$exePath -ArgumentList `$arguments -NoNewWindow -Wait }`""
186 | # Create a task named "Weekly Bourso CLI Task"
187 | Register-ScheduledTask -TaskName "Weekly Bourso CLI Task" -Trigger $trigger -Action $action
188 | ```
189 |
190 | ## Security
191 | This app runs locally. All outbound/inbound data is sent/received to/from BoursoBank servers **only**. Your password will not be saved locally and will be asked each time you run the app. Your client ID has to be configurated and will be saved into the app data for next usages.
192 |
193 | ## Disclaimer
194 |
195 | This script is provided as is, without any warranty. I am not responsible for any loss of funds. Use at your own risk. I am not affiliated with BoursoBank or any other project mentioned in this repository. This is not financial advice.
196 |
--------------------------------------------------------------------------------
/bourso-launchd.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Define your arguments here
4 | SYMBOL="1rTCW8"
5 | ACCOUNT="a583f3c5842c34fb00b408486ef493e0"
6 | QUANTITY="1"
7 | # ./bourso-cli trade order new --side buy --symbol 1rTCW8 --account a583f3c5842c34fb00b408486ef493e0 --quantity 4
8 |
9 | # Path to the bourso-cli script
10 | SCRIPT_PATH="/path/to/your/bourso-cli"
11 | # e.g SCRIPT_PATH="$HOME/bourso-cli-darwin"
12 |
13 | # Define the timestamp file path
14 | TIMESTAMP_FILE="$HOME/.bourso-cli/last_run"
15 |
16 | # Get the current timestamp
17 | CURRENT_TIME=$(date +%s)
18 |
19 | # Check if the timestamp file exists and read the last execution timestamp
20 | if [ -f "$TIMESTAMP_FILE" ]; then
21 | LAST_RUN=$(cat "$TIMESTAMP_FILE")
22 | else
23 | LAST_RUN=0
24 | fi
25 |
26 | # Time interval in seconds (1 week)
27 | INTERVAL=$((7 * 24 * 60 * 60))
28 |
29 | # If the time since the last run is greater than or equal to the interval, run the script
30 | if [ $((CURRENT_TIME - LAST_RUN)) -ge $INTERVAL ]; then
31 | # Launch the script with the terminal window "popping up"
32 | osascript < "$TIMESTAMP_FILE"
41 | fi
--------------------------------------------------------------------------------
/src/bourso_api/.gitignore:
--------------------------------------------------------------------------------
1 | /target
--------------------------------------------------------------------------------
/src/bourso_api/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.21.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler"
16 | version = "1.0.2"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
19 |
20 | [[package]]
21 | name = "aho-corasick"
22 | version = "1.1.2"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
25 | dependencies = [
26 | "memchr",
27 | ]
28 |
29 | [[package]]
30 | name = "android-tzdata"
31 | version = "0.1.1"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
34 |
35 | [[package]]
36 | name = "android_system_properties"
37 | version = "0.1.5"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
40 | dependencies = [
41 | "libc",
42 | ]
43 |
44 | [[package]]
45 | name = "anstream"
46 | version = "0.6.4"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44"
49 | dependencies = [
50 | "anstyle",
51 | "anstyle-parse",
52 | "anstyle-query",
53 | "anstyle-wincon",
54 | "colorchoice",
55 | "utf8parse",
56 | ]
57 |
58 | [[package]]
59 | name = "anstyle"
60 | version = "1.0.4"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
63 |
64 | [[package]]
65 | name = "anstyle-parse"
66 | version = "0.2.2"
67 | source = "registry+https://github.com/rust-lang/crates.io-index"
68 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140"
69 | dependencies = [
70 | "utf8parse",
71 | ]
72 |
73 | [[package]]
74 | name = "anstyle-query"
75 | version = "1.0.0"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
78 | dependencies = [
79 | "windows-sys",
80 | ]
81 |
82 | [[package]]
83 | name = "anstyle-wincon"
84 | version = "3.0.1"
85 | source = "registry+https://github.com/rust-lang/crates.io-index"
86 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
87 | dependencies = [
88 | "anstyle",
89 | "windows-sys",
90 | ]
91 |
92 | [[package]]
93 | name = "anyhow"
94 | version = "1.0.75"
95 | source = "registry+https://github.com/rust-lang/crates.io-index"
96 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
97 |
98 | [[package]]
99 | name = "autocfg"
100 | version = "1.1.0"
101 | source = "registry+https://github.com/rust-lang/crates.io-index"
102 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
103 |
104 | [[package]]
105 | name = "backtrace"
106 | version = "0.3.69"
107 | source = "registry+https://github.com/rust-lang/crates.io-index"
108 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
109 | dependencies = [
110 | "addr2line",
111 | "cc",
112 | "cfg-if",
113 | "libc",
114 | "miniz_oxide",
115 | "object",
116 | "rustc-demangle",
117 | ]
118 |
119 | [[package]]
120 | name = "base64"
121 | version = "0.21.5"
122 | source = "registry+https://github.com/rust-lang/crates.io-index"
123 | checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
124 |
125 | [[package]]
126 | name = "bitflags"
127 | version = "1.3.2"
128 | source = "registry+https://github.com/rust-lang/crates.io-index"
129 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
130 |
131 | [[package]]
132 | name = "bitflags"
133 | version = "2.4.1"
134 | source = "registry+https://github.com/rust-lang/crates.io-index"
135 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
136 |
137 | [[package]]
138 | name = "bourso_api"
139 | version = "0.1.0"
140 | dependencies = [
141 | "anyhow",
142 | "chrono",
143 | "clap",
144 | "cookie_store 0.20.0",
145 | "lazy_static",
146 | "log",
147 | "regex",
148 | "reqwest",
149 | "reqwest_cookie_store",
150 | "serde",
151 | "serde_json",
152 | ]
153 |
154 | [[package]]
155 | name = "bumpalo"
156 | version = "3.14.0"
157 | source = "registry+https://github.com/rust-lang/crates.io-index"
158 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
159 |
160 | [[package]]
161 | name = "bytes"
162 | version = "1.5.0"
163 | source = "registry+https://github.com/rust-lang/crates.io-index"
164 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
165 |
166 | [[package]]
167 | name = "cc"
168 | version = "1.0.83"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
171 | dependencies = [
172 | "libc",
173 | ]
174 |
175 | [[package]]
176 | name = "cfg-if"
177 | version = "1.0.0"
178 | source = "registry+https://github.com/rust-lang/crates.io-index"
179 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
180 |
181 | [[package]]
182 | name = "chrono"
183 | version = "0.4.31"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
186 | dependencies = [
187 | "android-tzdata",
188 | "iana-time-zone",
189 | "js-sys",
190 | "num-traits",
191 | "wasm-bindgen",
192 | "windows-targets",
193 | ]
194 |
195 | [[package]]
196 | name = "clap"
197 | version = "4.4.7"
198 | source = "registry+https://github.com/rust-lang/crates.io-index"
199 | checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b"
200 | dependencies = [
201 | "clap_builder",
202 | "clap_derive",
203 | ]
204 |
205 | [[package]]
206 | name = "clap_builder"
207 | version = "4.4.7"
208 | source = "registry+https://github.com/rust-lang/crates.io-index"
209 | checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663"
210 | dependencies = [
211 | "anstream",
212 | "anstyle",
213 | "clap_lex",
214 | "strsim",
215 | ]
216 |
217 | [[package]]
218 | name = "clap_derive"
219 | version = "4.4.7"
220 | source = "registry+https://github.com/rust-lang/crates.io-index"
221 | checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
222 | dependencies = [
223 | "heck",
224 | "proc-macro2",
225 | "quote",
226 | "syn",
227 | ]
228 |
229 | [[package]]
230 | name = "clap_lex"
231 | version = "0.6.0"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
234 |
235 | [[package]]
236 | name = "colorchoice"
237 | version = "1.0.0"
238 | source = "registry+https://github.com/rust-lang/crates.io-index"
239 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
240 |
241 | [[package]]
242 | name = "cookie"
243 | version = "0.16.2"
244 | source = "registry+https://github.com/rust-lang/crates.io-index"
245 | checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
246 | dependencies = [
247 | "percent-encoding",
248 | "time",
249 | "version_check",
250 | ]
251 |
252 | [[package]]
253 | name = "cookie"
254 | version = "0.17.0"
255 | source = "registry+https://github.com/rust-lang/crates.io-index"
256 | checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
257 | dependencies = [
258 | "percent-encoding",
259 | "time",
260 | "version_check",
261 | ]
262 |
263 | [[package]]
264 | name = "cookie_store"
265 | version = "0.16.2"
266 | source = "registry+https://github.com/rust-lang/crates.io-index"
267 | checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa"
268 | dependencies = [
269 | "cookie 0.16.2",
270 | "idna 0.2.3",
271 | "log",
272 | "publicsuffix",
273 | "serde",
274 | "serde_derive",
275 | "serde_json",
276 | "time",
277 | "url",
278 | ]
279 |
280 | [[package]]
281 | name = "cookie_store"
282 | version = "0.20.0"
283 | source = "registry+https://github.com/rust-lang/crates.io-index"
284 | checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6"
285 | dependencies = [
286 | "cookie 0.17.0",
287 | "idna 0.3.0",
288 | "log",
289 | "publicsuffix",
290 | "serde",
291 | "serde_derive",
292 | "serde_json",
293 | "time",
294 | "url",
295 | ]
296 |
297 | [[package]]
298 | name = "core-foundation"
299 | version = "0.9.3"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
302 | dependencies = [
303 | "core-foundation-sys",
304 | "libc",
305 | ]
306 |
307 | [[package]]
308 | name = "core-foundation-sys"
309 | version = "0.8.4"
310 | source = "registry+https://github.com/rust-lang/crates.io-index"
311 | checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
312 |
313 | [[package]]
314 | name = "deranged"
315 | version = "0.3.9"
316 | source = "registry+https://github.com/rust-lang/crates.io-index"
317 | checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
318 | dependencies = [
319 | "powerfmt",
320 | ]
321 |
322 | [[package]]
323 | name = "encoding_rs"
324 | version = "0.8.33"
325 | source = "registry+https://github.com/rust-lang/crates.io-index"
326 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
327 | dependencies = [
328 | "cfg-if",
329 | ]
330 |
331 | [[package]]
332 | name = "equivalent"
333 | version = "1.0.1"
334 | source = "registry+https://github.com/rust-lang/crates.io-index"
335 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
336 |
337 | [[package]]
338 | name = "errno"
339 | version = "0.3.5"
340 | source = "registry+https://github.com/rust-lang/crates.io-index"
341 | checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
342 | dependencies = [
343 | "libc",
344 | "windows-sys",
345 | ]
346 |
347 | [[package]]
348 | name = "fastrand"
349 | version = "2.0.1"
350 | source = "registry+https://github.com/rust-lang/crates.io-index"
351 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
352 |
353 | [[package]]
354 | name = "fnv"
355 | version = "1.0.7"
356 | source = "registry+https://github.com/rust-lang/crates.io-index"
357 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
358 |
359 | [[package]]
360 | name = "foreign-types"
361 | version = "0.3.2"
362 | source = "registry+https://github.com/rust-lang/crates.io-index"
363 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
364 | dependencies = [
365 | "foreign-types-shared",
366 | ]
367 |
368 | [[package]]
369 | name = "foreign-types-shared"
370 | version = "0.1.1"
371 | source = "registry+https://github.com/rust-lang/crates.io-index"
372 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
373 |
374 | [[package]]
375 | name = "form_urlencoded"
376 | version = "1.2.0"
377 | source = "registry+https://github.com/rust-lang/crates.io-index"
378 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
379 | dependencies = [
380 | "percent-encoding",
381 | ]
382 |
383 | [[package]]
384 | name = "futures-channel"
385 | version = "0.3.29"
386 | source = "registry+https://github.com/rust-lang/crates.io-index"
387 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
388 | dependencies = [
389 | "futures-core",
390 | ]
391 |
392 | [[package]]
393 | name = "futures-core"
394 | version = "0.3.29"
395 | source = "registry+https://github.com/rust-lang/crates.io-index"
396 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
397 |
398 | [[package]]
399 | name = "futures-sink"
400 | version = "0.3.29"
401 | source = "registry+https://github.com/rust-lang/crates.io-index"
402 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
403 |
404 | [[package]]
405 | name = "futures-task"
406 | version = "0.3.29"
407 | source = "registry+https://github.com/rust-lang/crates.io-index"
408 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
409 |
410 | [[package]]
411 | name = "futures-util"
412 | version = "0.3.29"
413 | source = "registry+https://github.com/rust-lang/crates.io-index"
414 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
415 | dependencies = [
416 | "futures-core",
417 | "futures-task",
418 | "pin-project-lite",
419 | "pin-utils",
420 | ]
421 |
422 | [[package]]
423 | name = "gimli"
424 | version = "0.28.0"
425 | source = "registry+https://github.com/rust-lang/crates.io-index"
426 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
427 |
428 | [[package]]
429 | name = "h2"
430 | version = "0.3.24"
431 | source = "registry+https://github.com/rust-lang/crates.io-index"
432 | checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
433 | dependencies = [
434 | "bytes",
435 | "fnv",
436 | "futures-core",
437 | "futures-sink",
438 | "futures-util",
439 | "http",
440 | "indexmap",
441 | "slab",
442 | "tokio",
443 | "tokio-util",
444 | "tracing",
445 | ]
446 |
447 | [[package]]
448 | name = "hashbrown"
449 | version = "0.14.3"
450 | source = "registry+https://github.com/rust-lang/crates.io-index"
451 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
452 |
453 | [[package]]
454 | name = "heck"
455 | version = "0.4.1"
456 | source = "registry+https://github.com/rust-lang/crates.io-index"
457 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
458 |
459 | [[package]]
460 | name = "http"
461 | version = "0.2.9"
462 | source = "registry+https://github.com/rust-lang/crates.io-index"
463 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
464 | dependencies = [
465 | "bytes",
466 | "fnv",
467 | "itoa",
468 | ]
469 |
470 | [[package]]
471 | name = "http-body"
472 | version = "0.4.5"
473 | source = "registry+https://github.com/rust-lang/crates.io-index"
474 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
475 | dependencies = [
476 | "bytes",
477 | "http",
478 | "pin-project-lite",
479 | ]
480 |
481 | [[package]]
482 | name = "httparse"
483 | version = "1.8.0"
484 | source = "registry+https://github.com/rust-lang/crates.io-index"
485 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
486 |
487 | [[package]]
488 | name = "httpdate"
489 | version = "1.0.3"
490 | source = "registry+https://github.com/rust-lang/crates.io-index"
491 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
492 |
493 | [[package]]
494 | name = "hyper"
495 | version = "0.14.27"
496 | source = "registry+https://github.com/rust-lang/crates.io-index"
497 | checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
498 | dependencies = [
499 | "bytes",
500 | "futures-channel",
501 | "futures-core",
502 | "futures-util",
503 | "h2",
504 | "http",
505 | "http-body",
506 | "httparse",
507 | "httpdate",
508 | "itoa",
509 | "pin-project-lite",
510 | "socket2 0.4.10",
511 | "tokio",
512 | "tower-service",
513 | "tracing",
514 | "want",
515 | ]
516 |
517 | [[package]]
518 | name = "hyper-tls"
519 | version = "0.5.0"
520 | source = "registry+https://github.com/rust-lang/crates.io-index"
521 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
522 | dependencies = [
523 | "bytes",
524 | "hyper",
525 | "native-tls",
526 | "tokio",
527 | "tokio-native-tls",
528 | ]
529 |
530 | [[package]]
531 | name = "iana-time-zone"
532 | version = "0.1.58"
533 | source = "registry+https://github.com/rust-lang/crates.io-index"
534 | checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
535 | dependencies = [
536 | "android_system_properties",
537 | "core-foundation-sys",
538 | "iana-time-zone-haiku",
539 | "js-sys",
540 | "wasm-bindgen",
541 | "windows-core",
542 | ]
543 |
544 | [[package]]
545 | name = "iana-time-zone-haiku"
546 | version = "0.1.2"
547 | source = "registry+https://github.com/rust-lang/crates.io-index"
548 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
549 | dependencies = [
550 | "cc",
551 | ]
552 |
553 | [[package]]
554 | name = "idna"
555 | version = "0.2.3"
556 | source = "registry+https://github.com/rust-lang/crates.io-index"
557 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
558 | dependencies = [
559 | "matches",
560 | "unicode-bidi",
561 | "unicode-normalization",
562 | ]
563 |
564 | [[package]]
565 | name = "idna"
566 | version = "0.3.0"
567 | source = "registry+https://github.com/rust-lang/crates.io-index"
568 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
569 | dependencies = [
570 | "unicode-bidi",
571 | "unicode-normalization",
572 | ]
573 |
574 | [[package]]
575 | name = "idna"
576 | version = "0.4.0"
577 | source = "registry+https://github.com/rust-lang/crates.io-index"
578 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
579 | dependencies = [
580 | "unicode-bidi",
581 | "unicode-normalization",
582 | ]
583 |
584 | [[package]]
585 | name = "indexmap"
586 | version = "2.1.0"
587 | source = "registry+https://github.com/rust-lang/crates.io-index"
588 | checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
589 | dependencies = [
590 | "equivalent",
591 | "hashbrown",
592 | ]
593 |
594 | [[package]]
595 | name = "ipnet"
596 | version = "2.9.0"
597 | source = "registry+https://github.com/rust-lang/crates.io-index"
598 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
599 |
600 | [[package]]
601 | name = "itoa"
602 | version = "1.0.9"
603 | source = "registry+https://github.com/rust-lang/crates.io-index"
604 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
605 |
606 | [[package]]
607 | name = "js-sys"
608 | version = "0.3.65"
609 | source = "registry+https://github.com/rust-lang/crates.io-index"
610 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
611 | dependencies = [
612 | "wasm-bindgen",
613 | ]
614 |
615 | [[package]]
616 | name = "lazy_static"
617 | version = "1.4.0"
618 | source = "registry+https://github.com/rust-lang/crates.io-index"
619 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
620 |
621 | [[package]]
622 | name = "libc"
623 | version = "0.2.149"
624 | source = "registry+https://github.com/rust-lang/crates.io-index"
625 | checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
626 |
627 | [[package]]
628 | name = "linux-raw-sys"
629 | version = "0.4.10"
630 | source = "registry+https://github.com/rust-lang/crates.io-index"
631 | checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
632 |
633 | [[package]]
634 | name = "log"
635 | version = "0.4.20"
636 | source = "registry+https://github.com/rust-lang/crates.io-index"
637 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
638 |
639 | [[package]]
640 | name = "matches"
641 | version = "0.1.10"
642 | source = "registry+https://github.com/rust-lang/crates.io-index"
643 | checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
644 |
645 | [[package]]
646 | name = "memchr"
647 | version = "2.6.4"
648 | source = "registry+https://github.com/rust-lang/crates.io-index"
649 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
650 |
651 | [[package]]
652 | name = "mime"
653 | version = "0.3.17"
654 | source = "registry+https://github.com/rust-lang/crates.io-index"
655 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
656 |
657 | [[package]]
658 | name = "mime_guess"
659 | version = "2.0.4"
660 | source = "registry+https://github.com/rust-lang/crates.io-index"
661 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
662 | dependencies = [
663 | "mime",
664 | "unicase",
665 | ]
666 |
667 | [[package]]
668 | name = "miniz_oxide"
669 | version = "0.7.1"
670 | source = "registry+https://github.com/rust-lang/crates.io-index"
671 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
672 | dependencies = [
673 | "adler",
674 | ]
675 |
676 | [[package]]
677 | name = "mio"
678 | version = "0.8.11"
679 | source = "registry+https://github.com/rust-lang/crates.io-index"
680 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
681 | dependencies = [
682 | "libc",
683 | "wasi",
684 | "windows-sys",
685 | ]
686 |
687 | [[package]]
688 | name = "native-tls"
689 | version = "0.2.11"
690 | source = "registry+https://github.com/rust-lang/crates.io-index"
691 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
692 | dependencies = [
693 | "lazy_static",
694 | "libc",
695 | "log",
696 | "openssl",
697 | "openssl-probe",
698 | "openssl-sys",
699 | "schannel",
700 | "security-framework",
701 | "security-framework-sys",
702 | "tempfile",
703 | ]
704 |
705 | [[package]]
706 | name = "num-traits"
707 | version = "0.2.17"
708 | source = "registry+https://github.com/rust-lang/crates.io-index"
709 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
710 | dependencies = [
711 | "autocfg",
712 | ]
713 |
714 | [[package]]
715 | name = "object"
716 | version = "0.32.1"
717 | source = "registry+https://github.com/rust-lang/crates.io-index"
718 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
719 | dependencies = [
720 | "memchr",
721 | ]
722 |
723 | [[package]]
724 | name = "once_cell"
725 | version = "1.18.0"
726 | source = "registry+https://github.com/rust-lang/crates.io-index"
727 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
728 |
729 | [[package]]
730 | name = "openssl"
731 | version = "0.10.60"
732 | source = "registry+https://github.com/rust-lang/crates.io-index"
733 | checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
734 | dependencies = [
735 | "bitflags 2.4.1",
736 | "cfg-if",
737 | "foreign-types",
738 | "libc",
739 | "once_cell",
740 | "openssl-macros",
741 | "openssl-sys",
742 | ]
743 |
744 | [[package]]
745 | name = "openssl-macros"
746 | version = "0.1.1"
747 | source = "registry+https://github.com/rust-lang/crates.io-index"
748 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
749 | dependencies = [
750 | "proc-macro2",
751 | "quote",
752 | "syn",
753 | ]
754 |
755 | [[package]]
756 | name = "openssl-probe"
757 | version = "0.1.5"
758 | source = "registry+https://github.com/rust-lang/crates.io-index"
759 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
760 |
761 | [[package]]
762 | name = "openssl-sys"
763 | version = "0.9.96"
764 | source = "registry+https://github.com/rust-lang/crates.io-index"
765 | checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
766 | dependencies = [
767 | "cc",
768 | "libc",
769 | "pkg-config",
770 | "vcpkg",
771 | ]
772 |
773 | [[package]]
774 | name = "percent-encoding"
775 | version = "2.3.0"
776 | source = "registry+https://github.com/rust-lang/crates.io-index"
777 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
778 |
779 | [[package]]
780 | name = "pin-project-lite"
781 | version = "0.2.13"
782 | source = "registry+https://github.com/rust-lang/crates.io-index"
783 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
784 |
785 | [[package]]
786 | name = "pin-utils"
787 | version = "0.1.0"
788 | source = "registry+https://github.com/rust-lang/crates.io-index"
789 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
790 |
791 | [[package]]
792 | name = "pkg-config"
793 | version = "0.3.27"
794 | source = "registry+https://github.com/rust-lang/crates.io-index"
795 | checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
796 |
797 | [[package]]
798 | name = "powerfmt"
799 | version = "0.2.0"
800 | source = "registry+https://github.com/rust-lang/crates.io-index"
801 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
802 |
803 | [[package]]
804 | name = "proc-macro2"
805 | version = "1.0.69"
806 | source = "registry+https://github.com/rust-lang/crates.io-index"
807 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
808 | dependencies = [
809 | "unicode-ident",
810 | ]
811 |
812 | [[package]]
813 | name = "psl-types"
814 | version = "2.0.11"
815 | source = "registry+https://github.com/rust-lang/crates.io-index"
816 | checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
817 |
818 | [[package]]
819 | name = "publicsuffix"
820 | version = "2.2.3"
821 | source = "registry+https://github.com/rust-lang/crates.io-index"
822 | checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
823 | dependencies = [
824 | "idna 0.3.0",
825 | "psl-types",
826 | ]
827 |
828 | [[package]]
829 | name = "quote"
830 | version = "1.0.33"
831 | source = "registry+https://github.com/rust-lang/crates.io-index"
832 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
833 | dependencies = [
834 | "proc-macro2",
835 | ]
836 |
837 | [[package]]
838 | name = "redox_syscall"
839 | version = "0.4.1"
840 | source = "registry+https://github.com/rust-lang/crates.io-index"
841 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
842 | dependencies = [
843 | "bitflags 1.3.2",
844 | ]
845 |
846 | [[package]]
847 | name = "regex"
848 | version = "1.10.2"
849 | source = "registry+https://github.com/rust-lang/crates.io-index"
850 | checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
851 | dependencies = [
852 | "aho-corasick",
853 | "memchr",
854 | "regex-automata",
855 | "regex-syntax",
856 | ]
857 |
858 | [[package]]
859 | name = "regex-automata"
860 | version = "0.4.3"
861 | source = "registry+https://github.com/rust-lang/crates.io-index"
862 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
863 | dependencies = [
864 | "aho-corasick",
865 | "memchr",
866 | "regex-syntax",
867 | ]
868 |
869 | [[package]]
870 | name = "regex-syntax"
871 | version = "0.8.2"
872 | source = "registry+https://github.com/rust-lang/crates.io-index"
873 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
874 |
875 | [[package]]
876 | name = "reqwest"
877 | version = "0.11.22"
878 | source = "registry+https://github.com/rust-lang/crates.io-index"
879 | checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
880 | dependencies = [
881 | "base64",
882 | "bytes",
883 | "cookie 0.16.2",
884 | "cookie_store 0.16.2",
885 | "encoding_rs",
886 | "futures-core",
887 | "futures-util",
888 | "h2",
889 | "http",
890 | "http-body",
891 | "hyper",
892 | "hyper-tls",
893 | "ipnet",
894 | "js-sys",
895 | "log",
896 | "mime",
897 | "mime_guess",
898 | "native-tls",
899 | "once_cell",
900 | "percent-encoding",
901 | "pin-project-lite",
902 | "serde",
903 | "serde_json",
904 | "serde_urlencoded",
905 | "system-configuration",
906 | "tokio",
907 | "tokio-native-tls",
908 | "tower-service",
909 | "url",
910 | "wasm-bindgen",
911 | "wasm-bindgen-futures",
912 | "web-sys",
913 | "winreg",
914 | ]
915 |
916 | [[package]]
917 | name = "reqwest_cookie_store"
918 | version = "0.6.0"
919 | source = "registry+https://github.com/rust-lang/crates.io-index"
920 | checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061"
921 | dependencies = [
922 | "bytes",
923 | "cookie_store 0.20.0",
924 | "reqwest",
925 | "url",
926 | ]
927 |
928 | [[package]]
929 | name = "rustc-demangle"
930 | version = "0.1.23"
931 | source = "registry+https://github.com/rust-lang/crates.io-index"
932 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
933 |
934 | [[package]]
935 | name = "rustix"
936 | version = "0.38.21"
937 | source = "registry+https://github.com/rust-lang/crates.io-index"
938 | checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
939 | dependencies = [
940 | "bitflags 2.4.1",
941 | "errno",
942 | "libc",
943 | "linux-raw-sys",
944 | "windows-sys",
945 | ]
946 |
947 | [[package]]
948 | name = "ryu"
949 | version = "1.0.15"
950 | source = "registry+https://github.com/rust-lang/crates.io-index"
951 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
952 |
953 | [[package]]
954 | name = "schannel"
955 | version = "0.1.22"
956 | source = "registry+https://github.com/rust-lang/crates.io-index"
957 | checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
958 | dependencies = [
959 | "windows-sys",
960 | ]
961 |
962 | [[package]]
963 | name = "security-framework"
964 | version = "2.9.2"
965 | source = "registry+https://github.com/rust-lang/crates.io-index"
966 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
967 | dependencies = [
968 | "bitflags 1.3.2",
969 | "core-foundation",
970 | "core-foundation-sys",
971 | "libc",
972 | "security-framework-sys",
973 | ]
974 |
975 | [[package]]
976 | name = "security-framework-sys"
977 | version = "2.9.1"
978 | source = "registry+https://github.com/rust-lang/crates.io-index"
979 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
980 | dependencies = [
981 | "core-foundation-sys",
982 | "libc",
983 | ]
984 |
985 | [[package]]
986 | name = "serde"
987 | version = "1.0.190"
988 | source = "registry+https://github.com/rust-lang/crates.io-index"
989 | checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
990 | dependencies = [
991 | "serde_derive",
992 | ]
993 |
994 | [[package]]
995 | name = "serde_derive"
996 | version = "1.0.190"
997 | source = "registry+https://github.com/rust-lang/crates.io-index"
998 | checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
999 | dependencies = [
1000 | "proc-macro2",
1001 | "quote",
1002 | "syn",
1003 | ]
1004 |
1005 | [[package]]
1006 | name = "serde_json"
1007 | version = "1.0.108"
1008 | source = "registry+https://github.com/rust-lang/crates.io-index"
1009 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
1010 | dependencies = [
1011 | "itoa",
1012 | "ryu",
1013 | "serde",
1014 | ]
1015 |
1016 | [[package]]
1017 | name = "serde_urlencoded"
1018 | version = "0.7.1"
1019 | source = "registry+https://github.com/rust-lang/crates.io-index"
1020 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1021 | dependencies = [
1022 | "form_urlencoded",
1023 | "itoa",
1024 | "ryu",
1025 | "serde",
1026 | ]
1027 |
1028 | [[package]]
1029 | name = "slab"
1030 | version = "0.4.9"
1031 | source = "registry+https://github.com/rust-lang/crates.io-index"
1032 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
1033 | dependencies = [
1034 | "autocfg",
1035 | ]
1036 |
1037 | [[package]]
1038 | name = "socket2"
1039 | version = "0.4.10"
1040 | source = "registry+https://github.com/rust-lang/crates.io-index"
1041 | checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
1042 | dependencies = [
1043 | "libc",
1044 | "winapi",
1045 | ]
1046 |
1047 | [[package]]
1048 | name = "socket2"
1049 | version = "0.5.5"
1050 | source = "registry+https://github.com/rust-lang/crates.io-index"
1051 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
1052 | dependencies = [
1053 | "libc",
1054 | "windows-sys",
1055 | ]
1056 |
1057 | [[package]]
1058 | name = "strsim"
1059 | version = "0.10.0"
1060 | source = "registry+https://github.com/rust-lang/crates.io-index"
1061 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
1062 |
1063 | [[package]]
1064 | name = "syn"
1065 | version = "2.0.38"
1066 | source = "registry+https://github.com/rust-lang/crates.io-index"
1067 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
1068 | dependencies = [
1069 | "proc-macro2",
1070 | "quote",
1071 | "unicode-ident",
1072 | ]
1073 |
1074 | [[package]]
1075 | name = "system-configuration"
1076 | version = "0.5.1"
1077 | source = "registry+https://github.com/rust-lang/crates.io-index"
1078 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
1079 | dependencies = [
1080 | "bitflags 1.3.2",
1081 | "core-foundation",
1082 | "system-configuration-sys",
1083 | ]
1084 |
1085 | [[package]]
1086 | name = "system-configuration-sys"
1087 | version = "0.5.0"
1088 | source = "registry+https://github.com/rust-lang/crates.io-index"
1089 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
1090 | dependencies = [
1091 | "core-foundation-sys",
1092 | "libc",
1093 | ]
1094 |
1095 | [[package]]
1096 | name = "tempfile"
1097 | version = "3.8.1"
1098 | source = "registry+https://github.com/rust-lang/crates.io-index"
1099 | checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
1100 | dependencies = [
1101 | "cfg-if",
1102 | "fastrand",
1103 | "redox_syscall",
1104 | "rustix",
1105 | "windows-sys",
1106 | ]
1107 |
1108 | [[package]]
1109 | name = "time"
1110 | version = "0.3.30"
1111 | source = "registry+https://github.com/rust-lang/crates.io-index"
1112 | checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
1113 | dependencies = [
1114 | "deranged",
1115 | "itoa",
1116 | "powerfmt",
1117 | "serde",
1118 | "time-core",
1119 | "time-macros",
1120 | ]
1121 |
1122 | [[package]]
1123 | name = "time-core"
1124 | version = "0.1.2"
1125 | source = "registry+https://github.com/rust-lang/crates.io-index"
1126 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
1127 |
1128 | [[package]]
1129 | name = "time-macros"
1130 | version = "0.2.15"
1131 | source = "registry+https://github.com/rust-lang/crates.io-index"
1132 | checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
1133 | dependencies = [
1134 | "time-core",
1135 | ]
1136 |
1137 | [[package]]
1138 | name = "tinyvec"
1139 | version = "1.6.0"
1140 | source = "registry+https://github.com/rust-lang/crates.io-index"
1141 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
1142 | dependencies = [
1143 | "tinyvec_macros",
1144 | ]
1145 |
1146 | [[package]]
1147 | name = "tinyvec_macros"
1148 | version = "0.1.1"
1149 | source = "registry+https://github.com/rust-lang/crates.io-index"
1150 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1151 |
1152 | [[package]]
1153 | name = "tokio"
1154 | version = "1.33.0"
1155 | source = "registry+https://github.com/rust-lang/crates.io-index"
1156 | checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653"
1157 | dependencies = [
1158 | "backtrace",
1159 | "bytes",
1160 | "libc",
1161 | "mio",
1162 | "pin-project-lite",
1163 | "socket2 0.5.5",
1164 | "windows-sys",
1165 | ]
1166 |
1167 | [[package]]
1168 | name = "tokio-native-tls"
1169 | version = "0.3.1"
1170 | source = "registry+https://github.com/rust-lang/crates.io-index"
1171 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
1172 | dependencies = [
1173 | "native-tls",
1174 | "tokio",
1175 | ]
1176 |
1177 | [[package]]
1178 | name = "tokio-util"
1179 | version = "0.7.10"
1180 | source = "registry+https://github.com/rust-lang/crates.io-index"
1181 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
1182 | dependencies = [
1183 | "bytes",
1184 | "futures-core",
1185 | "futures-sink",
1186 | "pin-project-lite",
1187 | "tokio",
1188 | "tracing",
1189 | ]
1190 |
1191 | [[package]]
1192 | name = "tower-service"
1193 | version = "0.3.2"
1194 | source = "registry+https://github.com/rust-lang/crates.io-index"
1195 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
1196 |
1197 | [[package]]
1198 | name = "tracing"
1199 | version = "0.1.40"
1200 | source = "registry+https://github.com/rust-lang/crates.io-index"
1201 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
1202 | dependencies = [
1203 | "pin-project-lite",
1204 | "tracing-core",
1205 | ]
1206 |
1207 | [[package]]
1208 | name = "tracing-core"
1209 | version = "0.1.32"
1210 | source = "registry+https://github.com/rust-lang/crates.io-index"
1211 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
1212 | dependencies = [
1213 | "once_cell",
1214 | ]
1215 |
1216 | [[package]]
1217 | name = "try-lock"
1218 | version = "0.2.4"
1219 | source = "registry+https://github.com/rust-lang/crates.io-index"
1220 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
1221 |
1222 | [[package]]
1223 | name = "unicase"
1224 | version = "2.7.0"
1225 | source = "registry+https://github.com/rust-lang/crates.io-index"
1226 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
1227 | dependencies = [
1228 | "version_check",
1229 | ]
1230 |
1231 | [[package]]
1232 | name = "unicode-bidi"
1233 | version = "0.3.13"
1234 | source = "registry+https://github.com/rust-lang/crates.io-index"
1235 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
1236 |
1237 | [[package]]
1238 | name = "unicode-ident"
1239 | version = "1.0.12"
1240 | source = "registry+https://github.com/rust-lang/crates.io-index"
1241 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
1242 |
1243 | [[package]]
1244 | name = "unicode-normalization"
1245 | version = "0.1.22"
1246 | source = "registry+https://github.com/rust-lang/crates.io-index"
1247 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
1248 | dependencies = [
1249 | "tinyvec",
1250 | ]
1251 |
1252 | [[package]]
1253 | name = "url"
1254 | version = "2.4.1"
1255 | source = "registry+https://github.com/rust-lang/crates.io-index"
1256 | checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
1257 | dependencies = [
1258 | "form_urlencoded",
1259 | "idna 0.4.0",
1260 | "percent-encoding",
1261 | ]
1262 |
1263 | [[package]]
1264 | name = "utf8parse"
1265 | version = "0.2.1"
1266 | source = "registry+https://github.com/rust-lang/crates.io-index"
1267 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
1268 |
1269 | [[package]]
1270 | name = "vcpkg"
1271 | version = "0.2.15"
1272 | source = "registry+https://github.com/rust-lang/crates.io-index"
1273 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
1274 |
1275 | [[package]]
1276 | name = "version_check"
1277 | version = "0.9.4"
1278 | source = "registry+https://github.com/rust-lang/crates.io-index"
1279 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
1280 |
1281 | [[package]]
1282 | name = "want"
1283 | version = "0.3.1"
1284 | source = "registry+https://github.com/rust-lang/crates.io-index"
1285 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
1286 | dependencies = [
1287 | "try-lock",
1288 | ]
1289 |
1290 | [[package]]
1291 | name = "wasi"
1292 | version = "0.11.0+wasi-snapshot-preview1"
1293 | source = "registry+https://github.com/rust-lang/crates.io-index"
1294 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1295 |
1296 | [[package]]
1297 | name = "wasm-bindgen"
1298 | version = "0.2.88"
1299 | source = "registry+https://github.com/rust-lang/crates.io-index"
1300 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
1301 | dependencies = [
1302 | "cfg-if",
1303 | "wasm-bindgen-macro",
1304 | ]
1305 |
1306 | [[package]]
1307 | name = "wasm-bindgen-backend"
1308 | version = "0.2.88"
1309 | source = "registry+https://github.com/rust-lang/crates.io-index"
1310 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
1311 | dependencies = [
1312 | "bumpalo",
1313 | "log",
1314 | "once_cell",
1315 | "proc-macro2",
1316 | "quote",
1317 | "syn",
1318 | "wasm-bindgen-shared",
1319 | ]
1320 |
1321 | [[package]]
1322 | name = "wasm-bindgen-futures"
1323 | version = "0.4.38"
1324 | source = "registry+https://github.com/rust-lang/crates.io-index"
1325 | checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
1326 | dependencies = [
1327 | "cfg-if",
1328 | "js-sys",
1329 | "wasm-bindgen",
1330 | "web-sys",
1331 | ]
1332 |
1333 | [[package]]
1334 | name = "wasm-bindgen-macro"
1335 | version = "0.2.88"
1336 | source = "registry+https://github.com/rust-lang/crates.io-index"
1337 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
1338 | dependencies = [
1339 | "quote",
1340 | "wasm-bindgen-macro-support",
1341 | ]
1342 |
1343 | [[package]]
1344 | name = "wasm-bindgen-macro-support"
1345 | version = "0.2.88"
1346 | source = "registry+https://github.com/rust-lang/crates.io-index"
1347 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
1348 | dependencies = [
1349 | "proc-macro2",
1350 | "quote",
1351 | "syn",
1352 | "wasm-bindgen-backend",
1353 | "wasm-bindgen-shared",
1354 | ]
1355 |
1356 | [[package]]
1357 | name = "wasm-bindgen-shared"
1358 | version = "0.2.88"
1359 | source = "registry+https://github.com/rust-lang/crates.io-index"
1360 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
1361 |
1362 | [[package]]
1363 | name = "web-sys"
1364 | version = "0.3.65"
1365 | source = "registry+https://github.com/rust-lang/crates.io-index"
1366 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85"
1367 | dependencies = [
1368 | "js-sys",
1369 | "wasm-bindgen",
1370 | ]
1371 |
1372 | [[package]]
1373 | name = "winapi"
1374 | version = "0.3.9"
1375 | source = "registry+https://github.com/rust-lang/crates.io-index"
1376 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1377 | dependencies = [
1378 | "winapi-i686-pc-windows-gnu",
1379 | "winapi-x86_64-pc-windows-gnu",
1380 | ]
1381 |
1382 | [[package]]
1383 | name = "winapi-i686-pc-windows-gnu"
1384 | version = "0.4.0"
1385 | source = "registry+https://github.com/rust-lang/crates.io-index"
1386 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1387 |
1388 | [[package]]
1389 | name = "winapi-x86_64-pc-windows-gnu"
1390 | version = "0.4.0"
1391 | source = "registry+https://github.com/rust-lang/crates.io-index"
1392 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1393 |
1394 | [[package]]
1395 | name = "windows-core"
1396 | version = "0.51.1"
1397 | source = "registry+https://github.com/rust-lang/crates.io-index"
1398 | checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
1399 | dependencies = [
1400 | "windows-targets",
1401 | ]
1402 |
1403 | [[package]]
1404 | name = "windows-sys"
1405 | version = "0.48.0"
1406 | source = "registry+https://github.com/rust-lang/crates.io-index"
1407 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1408 | dependencies = [
1409 | "windows-targets",
1410 | ]
1411 |
1412 | [[package]]
1413 | name = "windows-targets"
1414 | version = "0.48.5"
1415 | source = "registry+https://github.com/rust-lang/crates.io-index"
1416 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1417 | dependencies = [
1418 | "windows_aarch64_gnullvm",
1419 | "windows_aarch64_msvc",
1420 | "windows_i686_gnu",
1421 | "windows_i686_msvc",
1422 | "windows_x86_64_gnu",
1423 | "windows_x86_64_gnullvm",
1424 | "windows_x86_64_msvc",
1425 | ]
1426 |
1427 | [[package]]
1428 | name = "windows_aarch64_gnullvm"
1429 | version = "0.48.5"
1430 | source = "registry+https://github.com/rust-lang/crates.io-index"
1431 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1432 |
1433 | [[package]]
1434 | name = "windows_aarch64_msvc"
1435 | version = "0.48.5"
1436 | source = "registry+https://github.com/rust-lang/crates.io-index"
1437 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1438 |
1439 | [[package]]
1440 | name = "windows_i686_gnu"
1441 | version = "0.48.5"
1442 | source = "registry+https://github.com/rust-lang/crates.io-index"
1443 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1444 |
1445 | [[package]]
1446 | name = "windows_i686_msvc"
1447 | version = "0.48.5"
1448 | source = "registry+https://github.com/rust-lang/crates.io-index"
1449 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1450 |
1451 | [[package]]
1452 | name = "windows_x86_64_gnu"
1453 | version = "0.48.5"
1454 | source = "registry+https://github.com/rust-lang/crates.io-index"
1455 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1456 |
1457 | [[package]]
1458 | name = "windows_x86_64_gnullvm"
1459 | version = "0.48.5"
1460 | source = "registry+https://github.com/rust-lang/crates.io-index"
1461 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1462 |
1463 | [[package]]
1464 | name = "windows_x86_64_msvc"
1465 | version = "0.48.5"
1466 | source = "registry+https://github.com/rust-lang/crates.io-index"
1467 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1468 |
1469 | [[package]]
1470 | name = "winreg"
1471 | version = "0.50.0"
1472 | source = "registry+https://github.com/rust-lang/crates.io-index"
1473 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
1474 | dependencies = [
1475 | "cfg-if",
1476 | "windows-sys",
1477 | ]
1478 |
--------------------------------------------------------------------------------
/src/bourso_api/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "bourso_api"
3 | version = "0.1.11"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | reqwest = { version = "0.12.12", features = ["multipart", "cookies"] }
10 | anyhow = { version = "1.0.75" }
11 | regex = { version = "1.10.2" }
12 | lazy_static = { version = "1.4.0" }
13 | clap = { version = "4.5.26", features = ["derive"] }
14 | serde = { version = "1.0.189", features = ["derive"] }
15 | serde_json = { version = "1.0.107" }
16 | reqwest_cookie_store = "0.8.0"
17 | cookie_store = "0.21.1"
18 | chrono = { version = "0.4.39" }
19 | log = { version = "0.4.20" }
--------------------------------------------------------------------------------
/src/bourso_api/README.md:
--------------------------------------------------------------------------------
1 | # Bourso API
2 |
3 | Unofficial API for [BoursoBank/Boursorama](https://www.boursorama.com) written in Rust.
--------------------------------------------------------------------------------
/src/bourso_api/src/account.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | /// Type of account
4 | #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Default)]
5 | pub enum AccountKind {
6 | Banking,
7 | Savings,
8 | #[default]
9 | Trading,
10 | Loans,
11 | }
12 |
13 | /// A bank account
14 | #[derive(Serialize, Deserialize, Debug, Clone, Default)]
15 | pub struct Account {
16 | /// Account id as an hexadecimal string (32 characters)
17 | pub id: String,
18 | /// Account name
19 | pub name: String,
20 | /// Balance in cents
21 | pub balance: isize,
22 | /// Account bank name as you can connect accounts from other banks
23 | pub bank_name: String,
24 | /// The type of account
25 | pub kind: AccountKind,
26 | }
27 |
--------------------------------------------------------------------------------
/src/bourso_api/src/client/account.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | account::{AccountKind, Account},
3 | constants::{BASE_URL, SAVINGS_PATTERN, BANKING_PATTERN, TRADING_PATTERN, LOANS_PATTERN, ACCOUNT_PATTERN}
4 | };
5 |
6 | use super::BoursoWebClient;
7 |
8 | use anyhow::{Context, Result};
9 | use log::debug;
10 | use regex::Regex;
11 |
12 | impl BoursoWebClient {
13 | /// Get the accounts list.
14 | ///
15 | /// # Arguments
16 | ///
17 | /// * `kind` - The type of accounts to retrieve. If `None`, all accounts are retrieved.
18 | ///
19 | /// # Returns
20 | ///
21 | /// The accounts list as a vector of `Account`.
22 | pub async fn get_accounts(&self, kind: Option) -> Result> {
23 | let res = self.client
24 | .get(format!("{BASE_URL}/dashboard/liste-comptes?rumroute=dashboard.new_accounts&_hinclude=1"))
25 | .headers(self.get_headers())
26 | .send()
27 | .await?
28 | .text()
29 | .await?;
30 |
31 | let accounts = match kind {
32 | Some(AccountKind::Savings) => extract_accounts(&res, AccountKind::Savings)?,
33 | Some(AccountKind::Banking) => extract_accounts(&res, AccountKind::Banking)?,
34 | Some(AccountKind::Trading) => extract_accounts(&res, AccountKind::Trading)?,
35 | Some(AccountKind::Loans) => extract_accounts(&res, AccountKind::Loans)?,
36 | // all accounts
37 | _ => {
38 | [
39 | extract_accounts(&res, AccountKind::Savings).unwrap_or(Vec::new()),
40 | extract_accounts(&res, AccountKind::Banking).unwrap_or(Vec::new()),
41 | extract_accounts(&res, AccountKind::Trading).unwrap_or(Vec::new()),
42 | extract_accounts(&res, AccountKind::Loans).unwrap_or(Vec::new()),
43 | ].concat()
44 | },
45 | };
46 |
47 | Ok(accounts)
48 | }
49 | }
50 |
51 | fn extract_accounts(res: &str, kind: AccountKind) -> Result> {
52 | let regex = Regex::new(
53 | match kind {
54 | AccountKind::Savings => SAVINGS_PATTERN,
55 | AccountKind::Banking => BANKING_PATTERN,
56 | AccountKind::Trading => TRADING_PATTERN,
57 | AccountKind::Loans => LOANS_PATTERN,
58 | }
59 | )?;
60 | let accounts_ul = regex
61 | .captures(&res)
62 | .with_context(|| {
63 | debug!("Response: {}", res);
64 | format!("Failed to extract {:?} accounts from the response", kind)
65 | })?
66 | .get(1)
67 | .context("Failed to extract accounts from regex match")?
68 | .as_str();
69 |
70 | let account_regex = Regex::new(ACCOUNT_PATTERN)?;
71 |
72 | let accounts = account_regex
73 | .captures_iter(&accounts_ul)
74 | .map(|m| {
75 | Account {
76 | id: m.name("id")
77 | .unwrap()
78 | .as_str()
79 | .trim()
80 | .to_string(),
81 | name: m.name("name")
82 | .unwrap()
83 | .as_str()
84 | .trim()
85 | .to_string(),
86 | balance: m.name("balance")
87 | .unwrap()
88 | .as_str()
89 | .trim()
90 | .replace(" ", "")
91 | .replace(",", "")
92 | .replace("\u{a0}", "")
93 | .replace("−", "-")
94 | .parse::()
95 | .unwrap(),
96 | bank_name: m.name("bank_name")
97 | .unwrap()
98 | .as_str()
99 | .trim()
100 | .to_string(),
101 | kind: kind,
102 | }
103 | })
104 | .collect::>();
105 |
106 | Ok(accounts)
107 | }
108 |
109 | #[cfg(test)]
110 | mod tests {
111 | use crate::{client::account::extract_accounts, account::AccountKind};
112 |
113 | #[test]
114 | fn test_extract_accounts() {
115 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Savings).unwrap();
116 | assert_eq!(accounts.len(), 2);
117 | assert_eq!(accounts[0].name, "LIVRET DEVELOPPEMENT DURABLE SOLIDAIRE");
118 | assert_eq!(accounts[0].balance, 1101000);
119 | assert_eq!(accounts[0].bank_name, "BoursoBank");
120 | assert_eq!(accounts[1].id, "d4e4fd4067b6d4d0b538a15e42238ef9");
121 | assert_eq!(accounts[1].name, "Livret Jeune");
122 | assert_eq!(accounts[1].balance, 159972);
123 | assert_eq!(accounts[1].bank_name, "Crédit Agricole");
124 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Banking).unwrap();
125 | assert_eq!(accounts.len(), 2);
126 | assert_eq!(accounts[0].id, "e2f509c466f5294f15abd873dbbf8a62");
127 | assert_eq!(accounts[0].name, "BoursoBank");
128 | assert_eq!(accounts[0].balance, 2081050);
129 | assert_eq!(accounts[0].bank_name, "BoursoBank");
130 | assert_eq!(accounts[1].name, "Compte de chèques ****0102");
131 | assert_eq!(accounts[1].balance, 50040);
132 | assert_eq!(accounts[1].bank_name, "CIC");
133 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Trading).unwrap();
134 | assert_eq!(accounts.len(), 1);
135 | assert_eq!(accounts[0].name, "PEA DOE");
136 | let accounts = extract_accounts(ACCOUNTS_RES, AccountKind::Loans).unwrap();
137 | assert_eq!(accounts.len(), 1);
138 | assert_eq!(accounts[0].name, "Prêt personnel");
139 | assert_eq!(accounts[0].balance, -9495982);
140 | assert_eq!(accounts[0].bank_name, "Crédit Agricole");
141 | }
142 |
143 |
144 |
145 | pub const ACCOUNTS_RES: &str = r#"
147 |
148 |
149 |

151 |
152 |
153 |
154 |
155 |
226 |
227 |
228 |
286 |
287 |
288 |
325 |
326 |
327 |
364 |
365 |
366 |
367 |
368 |
369 |
370 | "#;
371 | }
--------------------------------------------------------------------------------
/src/bourso_api/src/client/config.rs:
--------------------------------------------------------------------------------
1 | use regex::Regex;
2 | use serde::{Serialize, Deserialize};
3 | use anyhow::{Result, Context};
4 |
5 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
6 | #[serde(rename_all = "camelCase")]
7 | pub struct Config {
8 | #[serde(rename = "API_HOST")]
9 | pub api_host: String,
10 | #[serde(rename = "API_PATH")]
11 | pub api_path: String,
12 | #[serde(rename = "API_ENV")]
13 | pub api_env: String,
14 | #[serde(rename = "API_URL")]
15 | pub api_url: String,
16 | #[serde(rename = "API_REFERER_FEATURE_ID")]
17 | pub api_referer_feature_id: String,
18 | #[serde(rename = "LOCALE")]
19 | pub locale: String,
20 | #[serde(rename = "SUBSCRIPTION_HOST")]
21 | pub subscription_host: String,
22 | #[serde(rename = "CUSTOMER_SUBSCRIPTION_HOST")]
23 | pub customer_subscription_host: String,
24 | #[serde(rename = "PROSPECT_SUBSCRIPTION_HOST")]
25 | pub prospect_subscription_host: String,
26 | #[serde(rename = "DEBUG")]
27 | pub debug: bool,
28 | #[serde(rename = "ENABLE_PROFILER")]
29 | pub enable_profiler: bool,
30 | #[serde(rename = "AUTHENTICATION_ENDPOINT")]
31 | pub authentication_endpoint: String,
32 | #[serde(rename = "app_customer_website_host")]
33 | pub app_customer_website_host: String,
34 | #[serde(rename = "app_portal_website_host")]
35 | pub app_portal_website_host: String,
36 | #[serde(rename = "pjax_enabled")]
37 | pub pjax_enabled: bool,
38 | #[serde(rename = "pjax_timeout")]
39 | pub pjax_timeout: i64,
40 | #[serde(rename = "pjax_offset_duration")]
41 | pub pjax_offset_duration: i64,
42 | #[serde(rename = "select_bar_autoclose_tooltip_timeout")]
43 | pub select_bar_autoclose_tooltip_timeout: i64,
44 | #[serde(rename = "app_release_date")]
45 | pub app_release_date: String,
46 | #[serde(rename = "USER_HASH")]
47 | pub user_hash: Option,
48 | #[serde(rename = "JWT_TOKEN_ID")]
49 | pub jwt_token_id: String,
50 | #[serde(rename = "DEFAULT_API_BEARER")]
51 | pub default_api_bearer: String,
52 | #[serde(rename = "JAVASCRIPT_APPS_BEARER")]
53 | pub javascript_apps_bearer: JavascriptAppsBearer,
54 | #[serde(rename = "APPLICATION_NAME")]
55 | pub application_name: String,
56 | pub webauth: Webauth,
57 | #[serde(rename = "MARKETING_NAME")]
58 | pub marketing_name: String,
59 | }
60 |
61 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
62 | #[serde(rename_all = "camelCase")]
63 | pub struct JavascriptAppsBearer {
64 | #[serde(rename = "web_all_feedback_01")]
65 | pub web_all_feedback_01: String,
66 | }
67 |
68 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
69 | #[serde(rename_all = "camelCase")]
70 | pub struct Webauth {
71 | pub prepare_path: String,
72 | pub valid_path: String,
73 | }
74 |
75 | pub fn extract_brs_config(res: &str) -> Result {
76 | let regex = Regex::new(r#"(?ms)window\.BRS_CONFIG\s*=\s*(?P.*?);"#).unwrap();
77 | let config = regex
78 | .captures(&res)
79 | .unwrap()
80 | .name("config")
81 | .unwrap();
82 |
83 | let config: Config = serde_json::from_str(&config.as_str().trim())
84 | .with_context(|| format!("Could not deserialize BRS_CONFIG: {}", config.as_str().trim()))?;
85 |
86 | Ok(config)
87 | }
88 |
89 | #[cfg(test)]
90 | mod tests {
91 | const SCRIPT_CONFIG: &str = r#""#;
98 |
99 | #[test]
100 | fn test_extract_brs_config() {
101 | let config = super::extract_brs_config(SCRIPT_CONFIG).unwrap();
102 | assert_eq!(config.jwt_token_id, "brsxds_61d55b52615fbdfb898a3731bba89b35");
103 | assert_eq!(config.api_path, "/services/api/v1.7");
104 | assert_eq!(config.api_host, "api.boursobank.com");
105 | assert_eq!(config.api_url, "https://api.boursobank.com/services/api/v1.7");
106 | assert_eq!(config.api_env, "prod");
107 | assert_eq!(config.api_referer_feature_id, "customer.dashboard_home.web_fr_front_20");
108 | assert_eq!(config.locale, "fr-FR");
109 | assert_eq!(config.subscription_host, "souscrire.boursobank.com");
110 | assert_eq!(config.customer_subscription_host, "souscrire.boursobank.com");
111 | assert_eq!(config.prospect_subscription_host, "ouvrir-un-compte.boursobank.com");
112 | assert_eq!(config.debug, false);
113 | assert_eq!(config.enable_profiler, false);
114 | assert_eq!(config.authentication_endpoint, "/connexion/");
115 | assert_eq!(config.app_customer_website_host, "clients.boursobank.com");
116 | assert_eq!(config.app_portal_website_host, "www.boursorama.com");
117 | assert_eq!(config.pjax_enabled, true);
118 | assert_eq!(config.pjax_timeout, 20000);
119 | assert_eq!(config.pjax_offset_duration, 350);
120 | assert_eq!(config.select_bar_autoclose_tooltip_timeout, 3000);
121 | assert_eq!(config.app_release_date, "2023-03-01T14:15:36+0100");
122 | assert_eq!(config.user_hash.unwrap(), "61d55b52615fbdf");
123 | assert_eq!(config.application_name, "web_fr_front_20");
124 | assert_eq!(config.marketing_name, "BoursoBank");
125 | assert_eq!(config.webauth.prepare_path, "/webauthn/authentification/preparation");
126 | assert_eq!(config.webauth.valid_path, "/webauthn/authentification/validation");
127 | assert_eq!(config.default_api_bearer, "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2OTgyNDU5MTksImV4cCI6MTcyOTc4MTkxOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.bvXls6bqw_xGqA6V8DQMsZK92dMrV8K6hebWpEu5IF8MlEd4qmwmcchJBUT7oeBnSIp5TJHH5112ho548Sw57A");
128 | }
129 | }
--------------------------------------------------------------------------------
/src/bourso_api/src/client/error.rs:
--------------------------------------------------------------------------------
1 | use std::error::Error;
2 | use std::fmt;
3 |
4 | #[derive(Debug)]
5 | pub enum ClientError {
6 | InvalidCredentials,
7 | MfaRequired,
8 | InvalidMfa,
9 | }
10 |
11 | impl fmt::Display for ClientError {
12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13 | match self {
14 | ClientError::InvalidCredentials => write!(f, "Invalid credentials"),
15 | ClientError::MfaRequired => write!(f, "MFA required"),
16 | ClientError::InvalidMfa => write!(f, "Invalid MFA"),
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/bourso_api/src/client/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod account;
2 | pub mod config;
3 | pub mod error;
4 | pub mod trade;
5 | pub mod virtual_pad;
6 |
7 | use core::fmt;
8 | use std::sync::Arc;
9 |
10 | use anyhow::{bail, Result};
11 | use cookie_store::Cookie;
12 | use error::ClientError;
13 | use log::{debug, error, info};
14 | use regex::Regex;
15 | use reqwest_cookie_store::{CookieStore, CookieStoreMutex};
16 | use serde::{Deserialize, Serialize};
17 |
18 | use self::config::{extract_brs_config, Config};
19 |
20 | use super::constants::BASE_URL;
21 |
22 | pub struct BoursoWebClient {
23 | /// The client used to make requests to the Bourso website.
24 | client: reqwest::Client,
25 | /// __brs_mit cookie is a cookie that is necessary to access login page.
26 | /// Bourso website sets it when you access the login page for the first time before refreshing the page.
27 | brs_mit_cookie: String,
28 | /// Virtual pad IDs are the IDs of the virtual pad keys. They are used to translate the password
29 | virtual_pad_ids: Vec,
30 | /// Challenge ID is a token retrieved from the virtual pad page. It represents a random string
31 | /// that corresponds to the used virtual pad keys layout.
32 | challenge_id: String,
33 | /// Customer ID used to login.
34 | customer_id: String,
35 | /// Form token used to login.
36 | token: String,
37 | /// Password used to login.
38 | password: String,
39 | /// Cookie store used to store cookies between each request made by the client to the Bourso website.
40 | cookie_store: Arc,
41 | /// Bourso Web current configuration
42 | pub config: Config,
43 | }
44 |
45 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46 | pub enum MfaType {
47 | Email,
48 | Sms,
49 | WebToApp,
50 | }
51 |
52 | impl fmt::Display for MfaType {
53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 | match self {
55 | MfaType::Email => write!(f, "email"),
56 | MfaType::Sms => write!(f, "sms"),
57 | MfaType::WebToApp => write!(f, "web to app"),
58 | }
59 | }
60 | }
61 |
62 | impl MfaType {
63 | pub fn start_path(&self) -> &'static str {
64 | match self {
65 | MfaType::Email => "startemail",
66 | MfaType::Sms => "startsms",
67 | MfaType::WebToApp => "startwebtoapp",
68 | }
69 | }
70 |
71 | pub fn check_path(&self) -> &'static str {
72 | match self {
73 | MfaType::Email => "checkemail",
74 | MfaType::Sms => "checksms",
75 | MfaType::WebToApp => "checkwebtoapp",
76 | }
77 | }
78 | }
79 |
80 | impl BoursoWebClient {
81 | pub fn new() -> BoursoWebClient {
82 | // create a new client
83 | let cookie_store = CookieStore::new(None);
84 | let cookie_store = CookieStoreMutex::new(cookie_store);
85 | let cookie_store = Arc::new(cookie_store);
86 | BoursoWebClient {
87 | client: reqwest::Client::builder()
88 | .redirect(reqwest::redirect::Policy::none())
89 | .cookie_provider(Arc::clone(&cookie_store))
90 | .build()
91 | .unwrap(),
92 | cookie_store: cookie_store,
93 | brs_mit_cookie: String::new(),
94 | virtual_pad_ids: Default::default(),
95 | challenge_id: String::new(),
96 | customer_id: String::new(),
97 | token: String::new(),
98 | password: String::new(),
99 | config: Config::default(),
100 | }
101 | }
102 |
103 | /// Get the headers needed to make requests to the Bourso website.
104 | ///
105 | /// # Returns
106 | ///
107 | /// The headers as a `reqwest::header::HeaderMap`.
108 | #[cfg(not(tarpaulin_include))]
109 | fn get_headers(&self) -> reqwest::header::HeaderMap {
110 | let mut headers = reqwest::header::HeaderMap::new();
111 | headers.insert(
112 | "user-agent",
113 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36".parse().unwrap(),
114 | );
115 |
116 | headers
117 | }
118 |
119 | /// Get the login page content as a string.
120 | ///
121 | /// We're forced to call this page at least two times to retrieve the `__brs_mit` cookie and the form token.
122 | ///
123 | /// # Returns
124 | ///
125 | /// The login page content as a string.
126 | #[cfg(not(tarpaulin_include))]
127 | async fn get_login_page(&self) -> Result {
128 | Ok(self
129 | .client
130 | .get(format!("{BASE_URL}/connexion/"))
131 | .headers(self.get_headers())
132 | .send()
133 | .await?
134 | .text()
135 | .await?)
136 | }
137 |
138 | /// Initialize the session by retrieving the `__brs_mit` cookie, the form token, the challenge ID and the virtual pad keys.
139 | ///
140 | /// # Returns
141 | ///
142 | /// Nothing if the session was initialized successfully, an error otherwise.
143 | #[cfg(not(tarpaulin_include))]
144 | pub async fn init_session(&mut self) -> Result<()> {
145 | // This first call is necessary to get the __brs_mit cookie
146 | let init_res = self.get_login_page().await?;
147 |
148 | self.brs_mit_cookie = extract_brs_mit_cookie(&init_res)?;
149 |
150 | // Use a scope to drop the lock on the cookie store
151 | // once we've inserted the necessary cookies
152 | {
153 | let mut store = self.cookie_store.lock().unwrap();
154 | store.insert(
155 | Cookie::parse(
156 | // Necessary cookie to remove the domain migration error
157 | "brsDomainMigration=migrated;",
158 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(),
159 | )
160 | .unwrap(),
161 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(),
162 | )?;
163 | store.insert(
164 | Cookie::parse(
165 | // Necessary cookie to access the virtual pad
166 | format!("__brs_mit={};", self.brs_mit_cookie),
167 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(),
168 | )
169 | .unwrap(),
170 | &reqwest::Url::parse(&format!("{BASE_URL}/")).unwrap(),
171 | )?;
172 | }
173 |
174 | // We call the login page again to a form token
175 | let res = self.get_login_page().await?;
176 |
177 | self.token = extract_token(&res)?;
178 | self.config = extract_brs_config(&res)?;
179 | debug!("Using version from {}", self.config.app_release_date);
180 |
181 | let res = self
182 | .client
183 | .get(format!("{BASE_URL}/connexion/clavier-virtuel?_hinclude=1"))
184 | .headers(self.get_headers())
185 | .send()
186 | .await?
187 | .text()
188 | .await?;
189 |
190 | self.challenge_id = virtual_pad::extract_challenge_token(&res)?;
191 |
192 | self.virtual_pad_ids = virtual_pad::extract_data_matrix_keys(&res)?
193 | .map(|key| key.to_string())
194 | .to_vec();
195 |
196 | Ok(())
197 | }
198 |
199 | /// Login to the Bourso website.
200 | ///
201 | /// # Arguments
202 | ///
203 | /// * `customer_id` - The customer ID used to login.
204 | /// * `password` - The password used to login in plaintext.
205 | ///
206 | /// # Returns
207 | ///
208 | /// Nothing if the login was successful, an error otherwise.
209 | #[cfg(not(tarpaulin_include))]
210 | pub async fn login(&mut self, customer_id: &str, password: &str) -> Result<()> {
211 | use error::ClientError;
212 |
213 | self.customer_id = customer_id.to_string();
214 | self.password =
215 | virtual_pad::password_to_virtual_pad_keys(self.virtual_pad_ids.clone(), password)?
216 | .join("|");
217 | let data = reqwest::multipart::Form::new()
218 | .text("form[fakePassword]", "••••••••")
219 | .text("form[ajx]", "1")
220 | .text("form[password]", self.password.clone())
221 | // passwordAck is a JSON object that indicates the different times the user pressed on the virtual pad keys,
222 | // the click coordinates and the screen size. It seems like it's not necessary to fill the values to login.
223 | .text("form[passwordAck]", r#"{"ry":[],"pt":[],"js":true}"#)
224 | .text("form[platformAuthenticatorAvailable]", "1")
225 | .text("form[matrixRandomChallenge]", self.challenge_id.to_string())
226 | .text("form[_token]", self.token.to_string())
227 | .text("form[clientNumber]", self.customer_id.to_string());
228 |
229 | let res = self
230 | .client
231 | .post(format!("{BASE_URL}/connexion/saisie-mot-de-passe"))
232 | .multipart(data)
233 | .headers(self.get_headers())
234 | .send()
235 | .await?;
236 |
237 | if res.status() != 302 {
238 | let status = res.status();
239 | let text = res.text().await?;
240 | if text.contains("Identifiant ou mot de passe invalide")
241 | || text.contains("Erreur d'authentification")
242 | {
243 | bail!(ClientError::InvalidCredentials);
244 | }
245 | error!("{}", text);
246 | bail!("Could not login to Bourso website, status code: {}", status);
247 | }
248 |
249 | let res = self
250 | .client
251 | .get(format!("{BASE_URL}/"))
252 | .headers(self.get_headers())
253 | .send()
254 | .await?
255 | .text()
256 | .await?;
257 |
258 | if res.contains(r#"href="/se-deconnecter""#) {
259 | // Update the config with user hash
260 | self.config = extract_brs_config(&res)?;
261 | info!(
262 | "🔓 You are now logged in with user: {}",
263 | self.config.user_hash.as_ref().unwrap()
264 | );
265 | } else {
266 | if res.contains("/securisation") {
267 | bail!(ClientError::MfaRequired)
268 | /*
269 | bail!(r#"Boursobank has flagged this connection as suspicious.
270 | You're likely trying to login from a new device or location.
271 | Password authentication is not allowed in this case.
272 | We're aware of this issue and are working on a fix (https://github.com/azerpas/bourso-api/pull/10).
273 |
274 | In the meantime, you can try to login to the website manually from your current location (ip address) to unblock the connection, and then retry here."#);
275 | */
276 | }
277 | debug!("{}", res);
278 |
279 | bail!(ClientError::InvalidCredentials)
280 | }
281 |
282 | Ok(())
283 | }
284 |
285 | /// Request the MFA code to be sent to the user.
286 | ///
287 | /// # Returns
288 | /// * `otp_id` - The OTP ID tied to the MFA request.
289 | /// * `token_form` - The token form to use to submit the MFA code.
290 | /// * `mfa_type` - The type of MFA requested.
291 | pub async fn request_mfa(&mut self) -> Result<(String, String, MfaType)> {
292 | let _ = self
293 | .client
294 | .get(format!("{BASE_URL}/securisation"))
295 | .headers(self.get_headers())
296 | .send()
297 | .await?;
298 |
299 | let _ = self
300 | .client
301 | .get(format!("{BASE_URL}/x-domain-authentification/set-cookie"))
302 | .headers(self.get_headers())
303 | .send()
304 | .await?;
305 |
306 | let _ = self
307 | .client
308 | .get(format!("{BASE_URL}/"))
309 | .headers(self.get_headers())
310 | .send()
311 | .await?;
312 |
313 | let _ = self
314 | .client
315 | .get(format!("{BASE_URL}/securisation/authentification/"))
316 | .headers(self.get_headers())
317 | .send()
318 | .await?;
319 |
320 | let res = self
321 | .client
322 | .get(format!(
323 | "{BASE_URL}/securisation/authentification/validation"
324 | ))
325 | .headers(self.get_headers())
326 | .send()
327 | .await?;
328 |
329 | let res = res.text().await?;
330 |
331 | let mfa_type = if res.contains("brs-otp-email") {
332 | MfaType::Email
333 | } else if res.contains("brs-otp-sms") {
334 | MfaType::Sms
335 | } else if res.contains("brs-otp-webtoapp") {
336 | MfaType::WebToApp
337 | } else {
338 | debug!("{}", res);
339 | bail!("Could not request MFA, MFA type not found");
340 | };
341 |
342 | self.config = extract_brs_config(&res)?;
343 | let start_otp_url = match extract_start_otp_url(&res) {
344 | Ok(url) => url,
345 | Err(_) => {
346 | let res = self
347 | .client
348 | .get(format!("{BASE_URL}/securisation/validation"))
349 | .headers(self.get_headers())
350 | .send()
351 | .await?
352 | .text()
353 | .await?;
354 |
355 | println!("securisation/validation response: {}", res);
356 |
357 | bail!("Could not request MFA, start sms otp url not found");
358 | }
359 | };
360 |
361 | let otp_id = start_otp_url.split("/").last().unwrap();
362 | let contact_number = match mfa_type {
363 | MfaType::WebToApp => "your phone app".to_string(),
364 | _ => extract_user_contact(&res)?,
365 | };
366 | let token_form = extract_token(&res)?;
367 |
368 | let url = format!(
369 | "{}/_user_/_{}_/session/otp/{}/{}",
370 | self.config.api_url,
371 | self.config.user_hash.as_ref().unwrap(),
372 | mfa_type.start_path(),
373 | otp_id
374 | );
375 | debug!("Requesting MFA to {} with url {}", contact_number, url);
376 |
377 | let res = self
378 | .client
379 | .post(url)
380 | .body("{}")
381 | .headers(self.get_headers())
382 | .send()
383 | .await?;
384 |
385 | if res.status() != 200 {
386 | bail!("Could not request MFA, status code: {}", res.status());
387 | }
388 |
389 | let body = res.text().await?;
390 | let json_body: serde_json::Value = serde_json::from_str(&body)?;
391 | if json_body["success"].as_bool().unwrap() {
392 | info!("{} MFA request sent to {}", mfa_type, contact_number);
393 | } else {
394 | error!("{}", json_body);
395 | bail!("Could not request MFA, response: {}", json_body);
396 | }
397 |
398 | Ok((otp_id.to_string(), token_form, mfa_type))
399 | }
400 |
401 | /// Submit the MFA code to the Bourso website.
402 | ///
403 | /// # Arguments
404 | /// * `mfa_type` - The type of MFA to submit.
405 | /// * `otp_id` - The OTP ID tied to the MFA request.
406 | /// * `code` - The MFA code to submit.
407 | /// * `token_form` - The token form to use to submit the MFA code.
408 | pub async fn submit_mfa(
409 | &mut self,
410 | mfa_type: MfaType,
411 | otp_id: String,
412 | code: String,
413 | token_form: String,
414 | ) -> Result<()> {
415 | let url = format!(
416 | "{}/_user_/_{}_/session/otp/{}/{}",
417 | self.config.api_url,
418 | self.config.user_hash.as_ref().unwrap(),
419 | mfa_type.check_path(),
420 | otp_id
421 | );
422 | debug!("Submitting MFA code to {}", url);
423 |
424 | let payload = serde_json::json!({
425 | "token": code
426 | });
427 | let res = self
428 | .client
429 | .post(url)
430 | .body(payload.to_string())
431 | .header("Content-Type", "application/json; charset=utf-8")
432 | .send()
433 | .await?;
434 |
435 | if res.status() != 200 {
436 | bail!("Could not submit MFA code, status code: {}", res.status());
437 | }
438 |
439 | let body = res.text().await?;
440 | let json_body: serde_json::Value = serde_json::from_str(&body)?;
441 |
442 | if json_body["success"].as_bool().unwrap() {
443 | debug!("Submitting form with token: {}", token_form);
444 |
445 | let params = [("form[_token]", token_form)];
446 |
447 | let res = self
448 | .client
449 | .post(format!(
450 | "{BASE_URL}/securisation/authentification/validation"
451 | ))
452 | .form(¶ms)
453 | .header("Host", "clients.boursobank.com")
454 | .header(
455 | "accept",
456 | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
457 | )
458 | .header("origin", "https://clients.boursobank.com")
459 | .header("sec-fetch-site", "same-origin")
460 | .header("sec-fetch-mode", "navigate")
461 | .headers(self.get_headers())
462 | .header(
463 | "referer",
464 | "https://clients.boursobank.com/securisation/authentification/validation",
465 | )
466 | .header("sec-fetch-dest", "document")
467 | .header("accept-language", "fr-FR,fr;q=0.9")
468 | .header("priority", "u=0, i")
469 | .send()
470 | .await?;
471 |
472 | if res.status() != 302 {
473 | // bail!("Could not submit MFA validation, status code: {}", res.status());
474 | println!(
475 | "Could not submit MFA validation, status code: {}",
476 | res.status()
477 | );
478 | }
479 |
480 | let res = self
481 | .client
482 | .get(format!("{BASE_URL}/"))
483 | .headers(self.get_headers())
484 | .header(
485 | "referer",
486 | format!("{BASE_URL}/securisation/authentification/validation"),
487 | )
488 | .header("accept-language", "fr-FR,fr;q=0.9")
489 | .send()
490 | .await?
491 | .text()
492 | .await?;
493 |
494 | if res.contains(r#"href="/se-deconnecter""#) {
495 | // Update the config with user hash
496 | self.config = extract_brs_config(&res)?;
497 | info!(
498 | "🔓 You are now logged in with user: {}",
499 | self.config.user_hash.as_ref().unwrap()
500 | );
501 | } else {
502 | if res.contains("/securisation") {
503 | bail!(ClientError::MfaRequired);
504 | }
505 |
506 | bail!("Could not submit MFA, response: {}", res);
507 | }
508 |
509 | info!("🔓 MFA successfully submitted");
510 | } else {
511 | error!("{}", json_body);
512 | bail!("Could not submit MFA, response: {}", json_body);
513 | }
514 |
515 | Ok(())
516 | }
517 | }
518 |
519 | /// Extract the __brs_mit cookie from a string, usually the response of the `/connexion/` page.
520 | ///
521 | /// # Arguments
522 | ///
523 | /// * `res` - The response of the `/connexion/` page as a string.
524 | ///
525 | /// # Returns
526 | ///
527 | /// The __brs_mit cookie as a string.
528 | fn extract_brs_mit_cookie(res: &str) -> Result {
529 | let regex = Regex::new(r"(?m)__brs_mit=(?P.*?);").unwrap();
530 | let captures = regex.captures(&res);
531 |
532 | if captures.is_none() {
533 | error!("{}", res);
534 | bail!("Could not extract brs mit cookie");
535 | }
536 |
537 | let brs_mit_cookie = captures.unwrap().name("brs_mit_cookie").unwrap();
538 |
539 | Ok(brs_mit_cookie.as_str().to_string())
540 | }
541 |
542 | fn extract_token(res: &str) -> Result {
543 | let regex = Regex::new(r#"(?ms)form\[_token\]"(.*?)value="(?P.*?)"\s*>"#).unwrap();
544 | let token = regex.captures(&res).unwrap().name("token").unwrap();
545 |
546 | Ok(token.as_str().trim().to_string())
547 | }
548 |
549 | fn extract_start_otp_url(res: &str) -> Result {
550 | let regex = Regex::new(r"(?m)\\/services\\/api\\/v[\d.]*?\\/_user_\\/_\{userHash\}_\\/session\\/otp\\/start.*?\\/\d+").unwrap();
551 |
552 | let captures = regex.captures(&res);
553 |
554 | if captures.is_none() {
555 | error!("{}", res);
556 | bail!("Could not extract start sms otp url");
557 | }
558 |
559 | let start_sms_otp_url = captures.unwrap().get(0).unwrap();
560 |
561 | Ok(start_sms_otp_url.as_str().replace("\\", "").to_string())
562 | }
563 |
564 | fn extract_user_contact(res: &str) -> Result {
565 | let regex = Regex::new(r"(?m)userContact":"(?P.*?)"").unwrap();
566 | let contact_user = regex.captures(&res).unwrap().name("contact_user").unwrap();
567 |
568 | Ok(contact_user.as_str().trim().to_string())
569 | }
570 |
571 | #[cfg(test)]
572 | mod tests {
573 | use super::*;
574 |
575 | #[test]
576 | fn test_get_headers() {
577 | let client = BoursoWebClient::new();
578 | let headers = client.get_headers();
579 | assert_eq!(headers.get("user-agent").unwrap().to_str().unwrap(), "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36");
580 | }
581 |
582 | #[test]
583 | fn test_extract_brs_mit_cookie() {
584 | let res = r#" \n\n\n \n\n\n\n\n\n"#;
585 | let brs_mit_cookie = extract_brs_mit_cookie(&res).unwrap();
586 | assert_eq!(brs_mit_cookie, "8e6912eb6a0268f0a2411668b8bf289f");
587 | }
588 |
589 | #[test]
590 | fn test_extract_token() {
591 | let res = r#"data-backspace>