diff --git a/.github/workflows/installer.yml b/.github/workflows/installer.yml new file mode 100644 index 0000000..a75c9a5 --- /dev/null +++ b/.github/workflows/installer.yml @@ -0,0 +1,33 @@ +name: Create Installer for The Onion Pack + +on: + release: + types: [published] + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@master + + - uses: actions/setup-python@v1 + with: + python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + + - name: Run setup.py to create sdist + run: python setup.py sdist --dist-dir installer + + - name: Run Inno Compiler + run: iscc "installer\theonionpack.iss" "/Dtheonionpack=." + + - name: Upload files to a GitHub release + uses: svenstaro/upload-release-action@1.0.1 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: installer\Output\TheOnionPack.exe + asset_name: TheOnionPack.exe + tag: ${{ github.ref }} + overwrite: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1be76b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/theonionpack.egg-info/ +/installer/Output +/dist \ No newline at end of file diff --git a/INDEPENDENCE b/INDEPENDENCE new file mode 100644 index 0000000..0ee20f9 --- /dev/null +++ b/INDEPENDENCE @@ -0,0 +1,8 @@ +Statement of Independence + +The Onion Pack +Copyright (c) 2019 - 2020 Ralph Wetzel + +This product is produced independently from the Tor(R) anonymity software and +carries no guarantee from The Tor Project about quality, suitability or anything +else. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..277f258 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +The MIT License (MIT) + +The Onion Pack +Copyright (c) 2019 - 2020 Ralph Wetzel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md index 4424356..e536bfe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,91 @@ # The Onion Pack -Tor Relay Bundle for Windows +This is The Onion Pack, a **Tor Relay Bundle** for Windows. +It allows you to install everything you need to run a Tor relay (or a Tor bridge) on your Windows computer system & offers you a smart interface to monitor and control your relay. + +> **Statement of Independence** +> This product is produced independently from the Tor(R) anonymity software and +carries no guarantee from [The Tor Project](www.torproject.org) about quality, suitability or anything +else. + +## Installation +To install The Onion Pack on your Windows computer, download & run [TheOnionPack.exe](https://github.com/ralphwetzel/theonionpack/releases/latest) , its installation programm. + +This installation program is going to perform a number of tasks: + +* Locate, download & install the latest Tor *Windows Expert Bundle* from the official sources at [torproject.org](https://www.torproject.org/download/tor/). +* Download & install a version of *Embeddable Python* from the official sources at [python.org](https://www.python.org/downloads/windows/). +* Setup an appropriate Python environment. +* Download & install [The Onion Box](http://www.theonionbox.com) (Dashboard to monitor Tor node operations) from the [Python Package Index](https://pypi.org/project/theonionbox/). + +and finally + +* Install The Onion Pack - a python script to control the Tor relay as well as The Onion Box. + +## Additional Activities to be performed prior Operation +There's - usually - one additional activity necessary to finish the setup of The Onion Pack & your new personal Tor relay: You need to tell your router / firewall to forward at least one port to your local Windows system: + +Tor - if operated as a relay or bridge - expects that clients can connect to its *ORPort*. If this port is not reachable from the voids of the internet, the relay will not announce it's presence - thus will not be of any use. Therefore you have to ensure that connections can be established to this *ORPort*. + +The default value for the *ORPort* of any Tor relay is **9001**. You may alter this via *torrc*. + +## Operation +When you run The Onion Pack, it launches your Tor relay - setup according to the configuration you defined - and The Onion Box. If both actions have been performed successfully, The Onion Pack puts an icon into the tray of your desktop. + +![image](documentation/toptray.png) + +This icon provides a context menu ... to monitor your Tor relay and to control it: + + +![image](documentation/topcontextmenu.png) + + +| Tray Menu Command | Action | +|---|---| +| **Monitor...** | Open The Onion Box, the dashboard to monitor your relay. Default (right click) action. +| Relay Control | +| Edit configuration file... | Opens *torrc*, the configuration file of your relay. You may edit & save this file to change the setup of your Tor relay. +| Show logfile... | Show the log messages of your Tor relay. This might be useful in case of trouble! +| Reload relay configuration...| If you've edited *torrc* to modify the configuration definition of your relay, you need to reload this configuration into the relay. +| Stop! | Terminate The Onion Pack + +## First Steps +By intension the Tor instance **initially** installed by The Onion Pack is **not operating in relay mode** - yet as a Tor client. +If you deliberately decide to establish a relay, edit the configuration file: **Tray menu > Relay Control > Edit configuration file...** +This will open an editor window - showing an empty file. + +Prerequisite to become a relay is the definition of an [*ORPort*](https://2019.www.torproject.org/docs/tor-manual.html.en#ORPort) : +``` +ORPort 9001 +``` +> Remember to define the port number in accordance to your port forwarding settings established at your router! + +Additionally you should at least give a name to your relay and define the [*ContactInfo*](https://2019.www.torproject.org/docs/tor-manual.html.en#ContactInfo) parameter. + +``` +ORPort 9001 +Nickname myRelay +ContactInfo mail at mymail dot com +``` + +As it is explicitely discouraged to run an Exit Relay on any computer system at home, you should - equally explicit - express your request to disable the exit functionality: + +``` +ORPort 9001 +Nickname myRelay +ContactInfo mail at mymail dot com +ExitRelay 0 +``` + +> Please make yourself familiar with the official documentation at [torproject.org](www.torproject.org), especially the [Tor Manual](https://2019.www.torproject.org/docs/tor-manual.html.en), to understand the capabilities of a Tor relay and the power of all its configuration parameters! + +You may & should define further configuration parameters ... and if done, save the modified *torrc*. + +To enable this configuration, you need to tell your Tor node to reload it's configuration file: **Tray menu > Relay Control > Reload relay configuration** + +Afterwards you may either check the logfile of your relay ( **Tray menu > Relay control > Show logfile...** ) or open the dashboard to monitor your relay: **Tray menu > Monitor...** + +Have fun! + + +## Thank you +I'd like to express my humble respect to @jordanrussel and @martjinlaan for their dedication to [Inno Setup](http://www.jrsoftware.org/isinfo.php). This is an amazing piece of software providing endless opportunities to create powerfull installers. Thanks a lot for your efforts to maintain this gem in code over years, offering it's brilliant capabilities to the community. \ No newline at end of file diff --git a/documentation/topcontextmenu.png b/documentation/topcontextmenu.png new file mode 100644 index 0000000..ac267f4 Binary files /dev/null and b/documentation/topcontextmenu.png differ diff --git a/documentation/toptray.png b/documentation/toptray.png new file mode 100644 index 0000000..889e956 Binary files /dev/null and b/documentation/toptray.png differ diff --git a/installer/IDP_1.5.1/COPYING.txt b/installer/IDP_1.5.1/COPYING.txt new file mode 100644 index 0000000..c62795c --- /dev/null +++ b/installer/IDP_1.5.1/COPYING.txt @@ -0,0 +1,11 @@ +Copyright (c) 2013-2015 Mitrich Software + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source distribution. \ No newline at end of file diff --git a/installer/IDP_1.5.1/ansi/idp.dll b/installer/IDP_1.5.1/ansi/idp.dll new file mode 100644 index 0000000..96a3531 Binary files /dev/null and b/installer/IDP_1.5.1/ansi/idp.dll differ diff --git a/installer/IDP_1.5.1/ansi/idplang/default.iss b/installer/IDP_1.5.1/ansi/idplang/default.iss new file mode 100644 index 0000000..e965c88 --- /dev/null +++ b/installer/IDP_1.5.1/ansi/idplang/default.iss @@ -0,0 +1,42 @@ +[CustomMessages] +IDP_FormCaption =Downloading additional files +IDP_FormDescription =Please wait while Setup is downloading additional files... +IDP_TotalProgress =Total progress +IDP_CurrentFile =Current file +IDP_File =File: +IDP_Speed =Speed: +IDP_Status =Status: +IDP_ElapsedTime =Elapsed time: +IDP_RemainingTime =Remaining time: +IDP_DetailsButton =Details +IDP_HideButton =Hide +IDP_RetryButton =Retry +IDP_IgnoreButton =Ignore +IDP_KBs =KB/s +IDP_MBs =MB/s +IDP_X_of_X =%.2f of %.2f +IDP_KB =KB +IDP_MB =MB +IDP_GB =GB +IDP_Initializing =Initializing... +IDP_GettingFileInformation=Getting file information... +IDP_StartingDownload =Starting download... +IDP_Connecting =Connecting... +IDP_Downloading =Downloading... +IDP_DownloadComplete =Download complete +IDP_DownloadFailed =Download failed +IDP_CannotConnect =Cannot connect +IDP_CancellingDownload =Cancelling download... +IDP_Unknown =Unknown +IDP_DownloadCancelled =Download cancelled +IDP_RetryNext =Check your connection and click 'Retry' to try downloading the files again, or click 'Next' to continue installing anyway. +IDP_RetryCancel =Check your connection and click 'Retry' to try downloading the files again, or click 'Cancel' to terminate setup. +IDP_FilesNotDownloaded =The following files were not downloaded: +IDP_HTTPError_X =HTTP error %d +IDP_400 =Bad request (400) +IDP_401 =Access denied (401) +IDP_404 =File not found (404) +IDP_407 =Proxy authentication required (407) +IDP_500 =Server internal error (500) +IDP_502 =Bad gateway (502) +IDP_503 =Service temporaily unavailable (503) diff --git a/installer/IDP_1.5.1/idp.iss b/installer/IDP_1.5.1/idp.iss new file mode 100644 index 0000000..580d5a8 --- /dev/null +++ b/installer/IDP_1.5.1/idp.iss @@ -0,0 +1,662 @@ +; Inno Download Plugin +; (c)2013-2014 Mitrich Software +; http://mitrichsoftware.wordpress.com/ +; https://code.google.com/p/inno-download-plugin/ + +#define IDPROOT ExtractFilePath(__PATHFILENAME__) + +#ifdef UNICODE + #pragma include __INCLUDE__ + ";" + IDPROOT + "\unicode" +#else + #pragma include __INCLUDE__ + ";" + IDPROOT + "\ansi" +#endif + +; If IDP_DEBUG is defined before including idp.iss, script will use debug version of idp.dll (not included, you need to build it yourself). +; Debug dll messages can be viewed with SysInternals DebugView (http://technet.microsoft.com/en-us/sysinternals/bb896647.aspx) +#ifdef IDP_DEBUG + #define DBGSUFFIX " debug" +#else + #define DBGSUFFIX +#endif + +#ifdef UNICODE + #define IDPDLLDIR IDPROOT + "\unicode" + DBGSUFFIX +#else + #define IDPDLLDIR IDPROOT + "\ansi" + DBGSUFFIX +#endif + +#define IDP_VER_MAJOR +#define IDP_VER_MINOR +#define IDP_VER_REV +#define IDP_VER_BUILD + +#expr ParseVersion(IDPDLLDIR + "\idp.dll", IDP_VER_MAJOR, IDP_VER_MINOR, IDP_VER_REV, IDP_VER_BUILD) +#define IDP_VER EncodeVer(IDP_VER_MAJOR, IDP_VER_MINOR, IDP_VER_REV, IDP_VER_BUILD) + +#define IDP_VER_STR GetFileVersion(IDPDLLDIR + "\idp.dll") + +[Files] +Source: "{#IDPDLLDIR}\idp.dll"; Flags: dontcopy; + +[Code] +procedure idpAddFile(url, filename: String); external 'idpAddFile@files:idp.dll cdecl'; +procedure idpAddFileComp(url, filename, components: String); external 'idpAddFileComp@files:idp.dll cdecl'; +procedure idpAddMirror(url, mirror: String); external 'idpAddMirror@files:idp.dll cdecl'; +procedure idpAddFtpDir(url, mask, destdir: String; recursive: Boolean); external 'idpAddFtpDir@files:idp.dll cdecl'; +procedure idpAddFtpDirComp(url, mask, destdir: String; recursive: Boolean; components: String); external 'idpAddFtpDirComp@files:idp.dll cdecl'; +procedure idpClearFiles; external 'idpClearFiles@files:idp.dll cdecl'; +function idpFilesCount: Integer; external 'idpFilesCount@files:idp.dll cdecl'; +function idpFtpDirsCount: Integer; external 'idpFtpDirsCount@files:idp.dll cdecl'; +function idpFileDownloaded(url: String): Boolean; external 'idpFileDownloaded@files:idp.dll cdecl'; +function idpFilesDownloaded: Boolean; external 'idpFilesDownloaded@files:idp.dll cdecl'; +function idpDownloadFile(url, filename: String): Boolean; external 'idpDownloadFile@files:idp.dll cdecl'; +function idpDownloadFiles: Boolean; external 'idpDownloadFiles@files:idp.dll cdecl'; +function idpDownloadFilesComp: Boolean; external 'idpDownloadFilesComp@files:idp.dll cdecl'; +function idpDownloadFilesCompUi: Boolean; external 'idpDownloadFilesCompUi@files:idp.dll cdecl'; +procedure idpStartDownload; external 'idpStartDownload@files:idp.dll cdecl'; +procedure idpStopDownload; external 'idpStopDownload@files:idp.dll cdecl'; +procedure idpSetLogin(login, password: String); external 'idpSetLogin@files:idp.dll cdecl'; +procedure idpSetProxyMode(mode: String); external 'idpSetProxyMode@files:idp.dll cdecl'; +procedure idpSetProxyName(name: String); external 'idpSetProxyName@files:idp.dll cdecl'; +procedure idpSetProxyLogin(login, password: String); external 'idpSetProxyLogin@files:idp.dll cdecl'; +procedure idpConnectControl(name: String; Handle: HWND); external 'idpConnectControl@files:idp.dll cdecl'; +procedure idpAddMessage(name, message: String); external 'idpAddMessage@files:idp.dll cdecl'; +procedure idpSetInternalOption(name, value: String); external 'idpSetInternalOption@files:idp.dll cdecl'; +procedure idpSetDetailedMode(mode: Boolean); external 'idpSetDetailedMode@files:idp.dll cdecl'; +procedure idpSetComponents(components: String); external 'idpSetComponents@files:idp.dll cdecl'; +procedure idpReportError; external 'idpReportError@files:idp.dll cdecl'; +procedure idpTrace(text: String); external 'idpTrace@files:idp.dll cdecl'; + +#if defined(UNICODE) && (Ver >= 0x05050300) +procedure idpAddFileSize(url, filename: String; size: Int64); external 'idpAddFileSize@files:idp.dll cdecl'; +procedure idpAddFileSizeComp(url, filename: String; size: Int64; components: String); external 'idpAddFileSize@files:idp.dll cdecl'; +function idpGetFileSize(url: String; var size: Int64): Boolean; external 'idpGetFileSize@files:idp.dll cdecl'; +function idpGetFilesSize(var size: Int64): Boolean; external 'idpGetFilesSize@files:idp.dll cdecl'; +#else +procedure idpAddFileSize(url, filename: String; size: Dword); external 'idpAddFileSize32@files:idp.dll cdecl'; +procedure idpAddFileSizeComp(url, filename: String; size: Dword; components: String); external 'idpAddFileSize32@files:idp.dll cdecl'; +function idpGetFileSize(url: String; var size: Dword): Boolean; external 'idpGetFileSize32@files:idp.dll cdecl'; +function idpGetFilesSize(var size: Dword): Boolean; external 'idpGetFilesSize32@files:idp.dll cdecl'; +#endif + +type TIdpForm = record + Page : TWizardPage; + TotalProgressBar : TNewProgressBar; + FileProgressBar : TNewProgressBar; + TotalProgressLabel: TNewStaticText; + CurrentFileLabel : TNewStaticText; + TotalDownloaded : TNewStaticText; + FileDownloaded : TNewStaticText; + FileNameLabel : TNewStaticText; + SpeedLabel : TNewStaticText; + StatusLabel : TNewStaticText; + ElapsedTimeLabel : TNewStaticText; + RemainingTimeLabel: TNewStaticText; + FileName : TNewStaticText; + Speed : TNewStaticText; + Status : TNewStaticText; + ElapsedTime : TNewStaticText; + RemainingTime : TNewStaticText; + DetailsButton : TNewButton; + GIDetailsButton : HWND; //Graphical Installer + DetailsVisible : Boolean; + InvisibleButton : TNewButton; + end; + + TIdpOptions = record + DetailedMode : Boolean; + NoDetailsButton: Boolean; + NoRetryButton : Boolean; + NoSkinnedButton: Boolean; //Graphical Installer + end; + +var IDPForm : TIdpForm; + IDPOptions: TIdpOptions; + +function StrToBool(value: String): Boolean; +var s: String; +begin + s := LowerCase(value); + + if s = 'true' then result := true + else if s = 't' then result := true + else if s = 'yes' then result := true + else if s = 'y' then result := true + else if s = 'false' then result := false + else if s = 'f' then result := false + else if s = 'no' then result := false + else if s = 'n' then result := false + else result := StrToInt(value) > 0; +end; + +function WizardVerySilent: Boolean; +var i: Integer; +begin + for i := 1 to ParamCount do + begin + if UpperCase(ParamStr(i)) = '/VERYSILENT' then + begin + result := true; + exit; + end; + end; + + result := false; +end; + +function WizardSupressMsgBoxes: Boolean; +var i: Integer; +begin + for i := 1 to ParamCount do + begin + if UpperCase(ParamStr(i)) = '/SUPPRESSMSGBOXES' then + begin + result := true; + exit; + end; + end; + + result := false; +end; + +procedure idpSetOption(name, value: String); +var key: String; +begin + key := LowerCase(name); + + if key = 'detailedmode' then IDPOptions.DetailedMode := StrToBool(value) + else if key = 'detailsvisible' then IDPOptions.DetailedMode := StrToBool(value) //alias + else if key = 'detailsbutton' then IDPOptions.NoDetailsButton := not StrToBool(value) + else if key = 'skinnedbutton' then IDPOptions.NoSkinnedButton := not StrToBool(value) + else if key = 'retrybutton' then + begin + IDPOptions.NoRetryButton := StrToInt(value) = 0; + idpSetInternalOption('RetryButton', value); + end + else + idpSetInternalOption(name, value); +end; + +procedure idpShowDetails(show: Boolean); +begin + IDPForm.FileProgressBar.Visible := show; + IDPForm.CurrentFileLabel.Visible := show; + IDPForm.FileDownloaded.Visible := show; + IDPForm.FileNameLabel.Visible := show; + IDPForm.SpeedLabel.Visible := show; + IDPForm.StatusLabel.Visible := show; + IDPForm.ElapsedTimeLabel.Visible := show; + IDPForm.RemainingTimeLabel.Visible := show; + IDPForm.FileName.Visible := show; + IDPForm.Speed.Visible := show; + IDPForm.Status.Visible := show; + IDPForm.ElapsedTime.Visible := show; + IDPForm.RemainingTime.Visible := show; + + IDPForm.DetailsVisible := show; + + if IDPForm.DetailsVisible then + begin + IDPForm.DetailsButton.Caption := ExpandConstant('{cm:IDP_HideButton}'); + IDPForm.DetailsButton.Top := ScaleY(184); + end + else + begin + IDPForm.DetailsButton.Caption := ExpandConstant('{cm:IDP_DetailsButton}'); + IDPForm.DetailsButton.Top := ScaleY(44); + end; + + idpSetDetailedMode(show); +end; + +procedure idpDetailsButtonClick(Sender: TObject); +begin + idpShowDetails(not IDPForm.DetailsVisible); +end; + +#ifdef GRAPHICAL_INSTALLER_PROJECT +procedure idpGIDetailsButtonClick(hButton: HWND); +begin + idpShowDetails(not IDPForm.DetailsVisible); + + if IDPForm.DetailsVisible then + begin + ButtonSetText(IDPForm.GIDetailsButton, PAnsiChar(ExpandConstant('{cm:IDP_HideButton}'))); + ButtonSetPosition(IDPForm.GIDetailsButton, IDPForm.DetailsButton.Left-ScaleX(5), ScaleY(184), ButtonWidth, ButtonHeight); + end + else + begin + ButtonSetText(IDPForm.GIDetailsButton, PAnsiChar(ExpandConstant('{cm:IDP_DetailsButton}'))); + ButtonSetPosition(IDPForm.GIDetailsButton, IDPForm.DetailsButton.Left-ScaleX(5), ScaleY(44), ButtonWidth, ButtonHeight); + end; + + ButtonRefresh(hButton); +end; + +procedure idpCreateGIDetailsButton; +var swButtonNormalColor : TColor; + swButtonFocusedColor : TColor; + swButtonPressedColor : TColor; + swButtonDisabledColor: TColor; +begin + swButtonNormalColor := SwitchColorFormat(ExpandConstant('{#ButtonNormalColor}')); + swButtonFocusedColor := SwitchColorFormat(ExpandConstant('{#ButtonFocusedColor}')); + swButtonPressedColor := SwitchColorFormat(ExpandConstant('{#ButtonPressedColor}')); + swButtonDisabledColor := SwitchColorFormat(ExpandConstant('{#ButtonDisabledColor}')); + + with IDPForm.DetailsButton do + begin + IDPForm.GIDetailsButton := ButtonCreate(IDPForm.Page.Surface.Handle, Left-ScaleX(5), Top, ButtonWidth, ButtonHeight, + ExpandConstant('{tmp}\{#ButtonPicture}'), coButtonShadow, False); + + ButtonSetEvent(IDPForm.GIDetailsButton, ButtonClickEventID, WrapButtonCallback(@idpGIDetailsButtonClick, 1)); + ButtonSetFont(IDPForm.GIDetailsButton, ButtonFont.Handle); + ButtonSetFontColor(IDPForm.GIDetailsButton, swButtonNormalColor, swButtonFocusedColor, swButtonPressedColor, swButtonDisabledColor); + ButtonSetText(IDPForm.GIDetailsButton, PAnsiChar(Caption)); + ButtonSetVisibility(IDPForm.GIDetailsButton, true); + ButtonSetEnabled(IDPForm.GIDetailsButton, true); + end; +end; +#endif + +procedure idpFormActivate(Page: TWizardPage); +begin + if WizardSilent then + idpSetOption('RetryButton', '0'); + + if WizardSupressMsgBoxes then + idpSetInternalOption('ErrorDialog', 'none'); + + if not IDPOptions.NoRetryButton then + WizardForm.BackButton.Caption := ExpandConstant('{cm:IDP_RetryButton}'); + + idpShowDetails(IDPOptions.DetailedMode); + IDPForm.DetailsButton.Visible := not IDPOptions.NoDetailsButton; + +#ifdef GRAPHICAL_INSTALLER_PROJECT + idpSetInternalOption('RedrawBackground', '1'); + idpConnectControl('GIBackButton', hBackButton); + idpConnectControl('GINextButton', hNextButton); + + if not IDPOptions.NoSkinnedButton then + begin + IDPForm.DetailsButton.Visible := false; + if IDPForm.GIDetailsButton = 0 then + idpCreateGIDetailsButton; + end; + + if IDPOptions.NoRetryButton then + WizardForm.BackButton.Enabled := false + else + WizardForm.BackButton.Visible := false; + + WizardForm.NextButton.Enabled := false; +#endif + idpSetComponents(WizardSelectedComponents(false)); + + if WizardVerySilent then + idpDownloadFilesComp + else if WizardSilent then + begin + WizardForm.Show; + WizardForm.Repaint; + idpDownloadFilesCompUi; + WizardForm.Hide; + end + else + idpStartDownload; +end; + +function idpShouldSkipPage(Page: TWizardPage): Boolean; +begin + idpSetComponents(WizardSelectedComponents(false)); + Result := ((idpFilesCount = 0) and (idpFtpDirsCount = 0)) or idpFilesDownloaded; +end; + +function idpBackButtonClick(Page: TWizardPage): Boolean; +begin + if not IDPOptions.NoRetryButton then // Retry button clicked + begin + idpStartDownload; + Result := False; + end + else + Result := true; +end; + +function idpNextButtonClick(Page: TWizardPage): Boolean; +begin + Result := True; +end; + +procedure idpCancelButtonClick(Page: TWizardPage; var Cancel, Confirm: Boolean); +begin + if ExitSetupMsgBox then + begin + IDPForm.Status.Caption := ExpandConstant('{cm:IDP_CancellingDownload}'); + WizardForm.Repaint; + idpStopDownload; + Cancel := true; + Confirm := false; + end + else + Cancel := false; +end; + +procedure idpReportErrorHelper(Sender: TObject); +begin + idpReportError; //calling idpReportError in main thread for compatibility with VCL Styles for IS +end; + +function idpCreateDownloadForm(PreviousPageId: Integer): Integer; +begin + IDPForm.Page := CreateCustomPage(PreviousPageId, ExpandConstant('{cm:IDP_FormCaption}'), ExpandConstant('{cm:IDP_FormDescription}')); + + IDPForm.TotalProgressBar := TNewProgressBar.Create(IDPForm.Page); + with IDPForm.TotalProgressBar do + begin + Parent := IDPForm.Page.Surface; + Left := ScaleX(0); + Top := ScaleY(16); + Width := ScaleX(410); + Height := ScaleY(20); + Min := 0; + Max := 100; + end; + + IDPForm.TotalProgressLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.TotalProgressLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_TotalProgress}'); + Left := ScaleX(0); + Top := ScaleY(0); + Width := ScaleX(200); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 1; + end; + + IDPForm.CurrentFileLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.CurrentFileLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_CurrentFile}'); + Left := ScaleX(0); + Top := ScaleY(48); + Width := ScaleX(200); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 2; + end; + + IDPForm.FileProgressBar := TNewProgressBar.Create(IDPForm.Page); + with IDPForm.FileProgressBar do + begin + Parent := IDPForm.Page.Surface; + Left := ScaleX(0); + Top := ScaleY(64); + Width := ScaleX(410); + Height := ScaleY(20); + Min := 0; + Max := 100; + end; + + IDPForm.TotalDownloaded := TNewStaticText.Create(IDPForm.Page); + with IDPForm.TotalDownloaded do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(290); + Top := ScaleY(0); + Width := ScaleX(120); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 4; + end; + + IDPForm.FileDownloaded := TNewStaticText.Create(IDPForm.Page); + with IDPForm.FileDownloaded do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(290); + Top := ScaleY(48); + Width := ScaleX(120); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 5; + end; + + IDPForm.FileNameLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.FileNameLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_File}'); + Left := ScaleX(0); + Top := ScaleY(100); + Width := ScaleX(116); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 6; + end; + + IDPForm.SpeedLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.SpeedLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_Speed}'); + Left := ScaleX(0); + Top := ScaleY(116); + Width := ScaleX(116); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 7; + end; + + IDPForm.StatusLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.StatusLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_Status}'); + Left := ScaleX(0); + Top := ScaleY(132); + Width := ScaleX(116); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 8; + end; + + IDPForm.ElapsedTimeLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.ElapsedTimeLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_ElapsedTime}'); + Left := ScaleX(0); + Top := ScaleY(148); + Width := ScaleX(116); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 9; + end; + + IDPForm.RemainingTimeLabel := TNewStaticText.Create(IDPForm.Page); + with IDPForm.RemainingTimeLabel do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_RemainingTime}'); + Left := ScaleX(0); + Top := ScaleY(164); + Width := ScaleX(116); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 10; + end; + + IDPForm.FileName := TNewStaticText.Create(IDPForm.Page); + with IDPForm.FileName do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(120); + Top := ScaleY(100); + Width := ScaleX(280); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 11; + end; + + IDPForm.Speed := TNewStaticText.Create(IDPForm.Page); + with IDPForm.Speed do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(120); + Top := ScaleY(116); + Width := ScaleX(280); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 12; + end; + + IDPForm.Status := TNewStaticText.Create(IDPForm.Page); + with IDPForm.Status do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(120); + Top := ScaleY(132); + Width := ScaleX(280); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 13; + end; + + IDPForm.ElapsedTime := TNewStaticText.Create(IDPForm.Page); + with IDPForm.ElapsedTime do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(120); + Top := ScaleY(148); + Width := ScaleX(280); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 14; + end; + + IDPForm.RemainingTime := TNewStaticText.Create(IDPForm.Page); + with IDPForm.RemainingTime do + begin + Parent := IDPForm.Page.Surface; + Caption := ''; + Left := ScaleX(120); + Top := ScaleY(164); + Width := ScaleX(280); + Height := ScaleY(14); + AutoSize := False; + TabOrder := 15; + end; + + IDPForm.DetailsButton := TNewButton.Create(IDPForm.Page); + with IDPForm.DetailsButton do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('{cm:IDP_DetailsButton}'); + Left := ScaleX(336); + Top := ScaleY(184); + Width := ScaleX(75); + Height := ScaleY(23); + TabOrder := 16; + OnClick := @idpDetailsButtonClick; + end; + + IDPForm.InvisibleButton := TNewButton.Create(IDPForm.Page); + with IDPForm.InvisibleButton do + begin + Parent := IDPForm.Page.Surface; + Caption := ExpandConstant('You must not see this button'); + Left := ScaleX(0); + Top := ScaleY(0); + Width := ScaleX(10); + Height := ScaleY(10); + TabOrder := 17; + Visible := False; + OnClick := @idpReportErrorHelper; + end; + + with IDPForm.Page do + begin + OnActivate := @idpFormActivate; + OnShouldSkipPage := @idpShouldSkipPage; + OnBackButtonClick := @idpBackButtonClick; + OnNextButtonClick := @idpNextButtonClick; + OnCancelButtonClick := @idpCancelButtonClick; + end; + + Result := IDPForm.Page.ID; +end; + +procedure idpConnectControls; +begin + idpConnectControl('TotalProgressLabel', IDPForm.TotalProgressLabel.Handle); + idpConnectControl('TotalProgressBar', IDPForm.TotalProgressBar.Handle); + idpConnectControl('FileProgressBar', IDPForm.FileProgressBar.Handle); + idpConnectControl('TotalDownloaded', IDPForm.TotalDownloaded.Handle); + idpConnectControl('FileDownloaded', IDPForm.FileDownloaded.Handle); + idpConnectControl('FileName', IDPForm.FileName.Handle); + idpConnectControl('Speed', IDPForm.Speed.Handle); + idpConnectControl('Status', IDPForm.Status.Handle); + idpConnectControl('ElapsedTime', IDPForm.ElapsedTime.Handle); + idpConnectControl('RemainingTime', IDPForm.RemainingTime.Handle); + idpConnectControl('InvisibleButton', IDPForm.InvisibleButton.Handle); + idpConnectControl('WizardPage', IDPForm.Page.Surface.Handle); + idpConnectControl('WizardForm', WizardForm.Handle); + idpConnectControl('BackButton', WizardForm.BackButton.Handle); + idpConnectControl('NextButton', WizardForm.NextButton.Handle); + idpConnectControl('LabelFont', IDPForm.TotalDownloaded.Font.Handle); +end; + +procedure idpInitMessages; +begin + idpAddMessage('Total progress', ExpandConstant('{cm:IDP_TotalProgress}')); + idpAddMessage('KB/s', ExpandConstant('{cm:IDP_KBs}')); + idpAddMessage('MB/s', ExpandConstant('{cm:IDP_MBs}')); + idpAddMessage('%.2f of %.2f', ExpandConstant('{cm:IDP_X_of_X}')); + idpAddMessage('KB', ExpandConstant('{cm:IDP_KB}')); + idpAddMessage('MB', ExpandConstant('{cm:IDP_MB}')); + idpAddMessage('GB', ExpandConstant('{cm:IDP_GB}')); + idpAddMessage('Initializing...', ExpandConstant('{cm:IDP_Initializing}')); + idpAddMessage('Getting file information...', ExpandConstant('{cm:IDP_GettingFileInformation}')); + idpAddMessage('Starting download...', ExpandConstant('{cm:IDP_StartingDownload}')); + idpAddMessage('Connecting...', ExpandConstant('{cm:IDP_Connecting}')); + idpAddMessage('Downloading...', ExpandConstant('{cm:IDP_Downloading}')); + idpAddMessage('Download complete', ExpandConstant('{cm:IDP_DownloadComplete}')); + idpAddMessage('Download failed', ExpandConstant('{cm:IDP_DownloadFailed}')); + idpAddMessage('Cannot connect', ExpandConstant('{cm:IDP_CannotConnect}')); + idpAddMessage('Unknown', ExpandConstant('{cm:IDP_Unknown}')); + idpAddMessage('Download cancelled', ExpandConstant('{cm:IDP_DownloadCancelled}')); + idpAddMessage('HTTP error %d', ExpandConstant('{cm:IDP_HTTPError_X}')); + idpAddMessage('400', ExpandConstant('{cm:IDP_400}')); + idpAddMessage('401', ExpandConstant('{cm:IDP_401}')); + idpAddMessage('404', ExpandConstant('{cm:IDP_404}')); + idpAddMessage('407', ExpandConstant('{cm:IDP_407}')); + idpAddMessage('500', ExpandConstant('{cm:IDP_500}')); + idpAddMessage('502', ExpandConstant('{cm:IDP_502}')); + idpAddMessage('503', ExpandConstant('{cm:IDP_503}')); + idpAddMessage('Retry', ExpandConstant('{cm:IDP_RetryButton}')); + idpAddMessage('Ignore', ExpandConstant('{cm:IDP_IgnoreButton}')); + idpAddMessage('Cancel', SetupMessage(msgButtonCancel)); + idpAddMessage('The following files were not downloaded:', ExpandConstant('{cm:IDP_FilesNotDownloaded}')); + idpAddMessage('Check your connection and click ''Retry'' to try downloading the files again, or click ''Next'' to continue installing anyway.', ExpandConstant('{cm:IDP_RetryNext}')); + idpAddMessage('Check your connection and click ''Retry'' to try downloading the files again, or click ''Cancel'' to terminate setup.', ExpandConstant('{cm:IDP_RetryCancel}')); +end; + +procedure idpDownloadAfter(PageAfterId: Integer); +begin + idpCreateDownloadForm(PageAfterId); + idpConnectControls; + idpInitMessages; +end; + +#include diff --git a/installer/IDP_1.5.1/unicode/idp.dll b/installer/IDP_1.5.1/unicode/idp.dll new file mode 100644 index 0000000..ce7b5da Binary files /dev/null and b/installer/IDP_1.5.1/unicode/idp.dll differ diff --git a/installer/IDP_1.5.1/unicode/idplang/default.iss b/installer/IDP_1.5.1/unicode/idplang/default.iss new file mode 100644 index 0000000..d847b86 --- /dev/null +++ b/installer/IDP_1.5.1/unicode/idplang/default.iss @@ -0,0 +1,42 @@ +[CustomMessages] +IDP_FormCaption =Downloading additional files +IDP_FormDescription =Please wait while Setup is downloading additional files... +IDP_TotalProgress =Total progress +IDP_CurrentFile =Current file +IDP_File =File: +IDP_Speed =Speed: +IDP_Status =Status: +IDP_ElapsedTime =Elapsed time: +IDP_RemainingTime =Remaining time: +IDP_DetailsButton =Details +IDP_HideButton =Hide +IDP_RetryButton =Retry +IDP_IgnoreButton =Ignore +IDP_KBs =KB/s +IDP_MBs =MB/s +IDP_X_of_X =%.2f of %.2f +IDP_KB =KB +IDP_MB =MB +IDP_GB =GB +IDP_Initializing =Initializing... +IDP_GettingFileInformation=Getting file information... +IDP_StartingDownload =Starting download... +IDP_Connecting =Connecting... +IDP_Downloading =Downloading... +IDP_DownloadComplete =Download complete +IDP_DownloadFailed =Download failed +IDP_CannotConnect =Cannot connect +IDP_CancellingDownload =Cancelling download... +IDP_Unknown =Unknown +IDP_DownloadCancelled =Download cancelled +IDP_RetryNext =Check your connection and click 'Retry' to try downloading the files again, or click 'Next' to continue installing anyway. +IDP_RetryCancel =Check your connection and click 'Retry' to try downloading the files again, or click 'Cancel' to terminate setup. +IDP_FilesNotDownloaded =The following files were not downloaded: +IDP_HTTPError_X =HTTP error %d +IDP_400 =Bad request (400) +IDP_401 =Access denied (401) +IDP_404 =File not found (404) +IDP_407 =Proxy authentication required (407) +IDP_500 =Server internal error (500) +IDP_502 =Bad gateway (502) +IDP_503 =Service temporaily unavailable (503) diff --git a/installer/theonionpack.iss b/installer/theonionpack.iss new file mode 100644 index 0000000..da436d9 --- /dev/null +++ b/installer/theonionpack.iss @@ -0,0 +1,680 @@ +#include <.\IDP_1.5.1\idp.iss> + +; The Onion Pack +; Definition file for the Inno Setup compiler. +; Copyright 2019 - 2020 Ralph Wetzel +; License MIT +; https://www.github.com/ralphwetzel/theonionpack + +; ===== +; Supported COMPILER command line parameter: +; "/Dtheonionpack=": To include a locally (at installer compilation time) +; provided package of theonionpack into the installer. +; The installer will only include a package with matching +; version number & adequate labeling! + +; ===== +; Supported INSTALLER command line parameters: +; /tob="theonionbox-xx.x.tar.gz": To install from a locally (at setup time) provided packache of The Onion Box +; (rather then pip'ing this from online). +; /top="theonionpack-xx.x.tar.gz": To install a locally (at setup time) provided packache of The Onion Pack +; (rather then using the one from this installer or pip'ing it from online). + +; All default INSTALLER commandline options are supported as well. +; In case of trouble - to enable logging - use: +; /LOG Create a log file in the user's TEMP directory +; /LOG="filename" Create a log file at the specified path. +; For further reference: http://www.jrsoftware.org/ishelp/index.php?topic=setupcmdline + +; ===== +; The Python version to be used is configured via an INI file. +; This ensures that compatibility can be tested ... to avoid side effects. +#define INIFile RemoveBackslash(SourcePath) + "\..\theonionpack\setup.ini" + +; The license file +#define LicenseFile RemoveBackslash(SourcePath) + "\..\LICENSE" + +; Statement of Independence +#define IndependenceFile RemoveBackslash(SourcePath) + "\..\INDEPENDENCE" + +; py shall become something like '3.7.6' +#define py ReadIni(INIFile, "python", "version") +; for pth we extract the first two digits of py +#define pth Copy(StringChange(py, '.', ''), 1, 2) + +; Tor Download page +#define tor ReadIni(INIFile, "tor", "download") + +; Name / Title +#define __title__ ReadIni(INIFile, "theonionpack", "title") +; Version +#define __version__ ReadIni(INIFile, "theonionpack", "version") +; Description +#define __description__ ReadIni(INIFile, "theonionpack", "description") +; Copyright +#define __copyright__ ReadIni(INIFile, "theonionpack", "copyright") + +[ThirdParty] +UseRelativePaths=True + +[Setup] +AppName={# __title__ } +AppVersion={# __version__} +AppCopyright={# __copyright__ } +AppId={{9CF06087-6B33-44B0-B9EE-24A3EE0678C9} +DefaultDirName={userpf}\TheOnionPack +DisableWelcomePage=False +UninstallLogMode=new +PrivilegesRequired=lowest +; There's a 'bug' (better an annoyance) in Inno Script Studio that limits +; ExtraDiskSpaceRequired to 10000000 in the dialog window. +; It yet doesn't overwrite the value here - as long as we don't touch it. +ExtraDiskSpaceRequired=87439216 +MinVersion=0,6.0 +LicenseFile={# LicenseFile} +WizardImageFile=compiler:WizModernImage-IS.bmp +WizardSmallImageFile=compiler:WizModernSmallImage-IS.bmp +OutputBaseFilename=TheOnionPack +DefaultGroupName=The Onion Pack +AppPublisher=Ralph Wetzel +AppComments={# __description__} +VersionInfoVersion={# __version__} +VersionInfoDescription={# __description__} +VersionInfoProductName={# __title__} +VersionInfoCopyright={# __copyright__} +DisableProgramGroupPage=yes + + +[Files] +; The statement of Independence; only used by the installer. +Source: "{# IndependenceFile}"; DestName: "INDEPENDENCE"; Flags: dontcopy +; +; Those file were downloaded & unziped into the {tmp} directory. +; Will be copied to {app} now. Inno keeps record of these files for later uninstall. +Source: "{tmp}\Python\*"; DestDir: "{app}\Python"; Flags: external recursesubdirs +Source: "{tmp}\Tor\*"; DestDir: "{app}\Tor"; Flags: external recursesubdirs +Source: "{tmp}\get-pip.py"; DestDir: "{app}\Python"; Flags: external deleteafterinstall +; +; The next line supports a CommandLine parameter for the Inno Setup COMPILER! +; This can be invoked by "/Dtheonionpack=" +; If defined, this package will become part of the installer. +; If not, we'll pip the package - either local or from PyPI +#ifdef theonionpack + #define top_file ExtractFilePath(theonionpack) + 'theonionpack-' + __version__ + '.tar.gz' + #if FileExists(top_file) + #define theonionpack top_file + #pragma message "TheOnionPack package @ '" + theonionpack + "' will be included in this installer." + Source: "{# theonionpack}"; DestDir: "{app}\Python"; DestName: "{# ExtractFileName(theonionpack)}"; Flags: deleteafterinstall + #else + #pragma error "FileNotFound: TheOnionPack package @ '" + theonionpack + "'!" + #undef theonionpack + #endif +#endif +; +; local package of TheOnionBox: CommandLine parameter to the INSTALLER +; As the Inno compiler is changing the CurrentWorkingDirectory (due to whatever reason) while processing, +; GetAbsSourcePath was added to work with the absolute path of a file - if the input is relative or absolute. +; CheckIfExists as well calls GetAbsSourcePath to verify file existance. +Source: "{code:GetAbsSourcePath|{param:tob}}"; \ + DestDir: "{app}\Python"; \ + DestName: "{code:ExtractFN|{param:tob}}"; \ + Flags: external deleteafterinstall; \ + Check: CheckIfExists(ExpandConstant('{param:tob}')) + +; local package of TheOnionPack: CommandLine parameter to the INSTALLER +Source: "{code:GetAbsSourcePath|{param:tob}}"; \ + DestDir: "{app}\Python"; \ + DestName: "{code:ExtractFN|{param:top}}"; \ + Flags: external deleteafterinstall; \ + Check: CheckIfExists(ExpandConstant('{param:top}')) + +; An icon ... +; Source: "..\theonionpack\icons\top256.ico"; DestDir: "{app}"; Attribs: hidden + + +[Dirs] +; Those two directories hold the data of the Tor relay (e.g. fingerprints). +; We'll never touch them! +Name: "{app}\Data"; Flags: uninsneveruninstall +Name: "{app}\Data\torrc"; Flags: uninsneveruninstall + +[Icons] +; This link gets the path to the Tor as a command line parameter. +Name: "{app}\The Onion Pack"; \ + Filename: "{app}\Python\Scripts\theonionpack.exe"; \ + WorkingDir: "{app}"; \ + Flags: runminimized; \ + Parameters: "--tor ""{app}\Tor"""; \ + Comment: "Launching The Onion Pack..." + +; This autostart link gets the path to the Tor as a command line parameter. +Name: "{userstartup}\The Onion Pack"; \ + Filename: "{app}\Python\Scripts\theonionpack.exe"; \ + WorkingDir: "{app}"; \ + Flags: runminimized; \ + Parameters: "--tor ""{app}\Tor"""; \ + Comment: "Launching The Onion Pack..."; \ + Tasks: startup + +; And finally: A desktop icon... +Name: "{userdesktop}\The Onion Pack"; \ + Filename: "{app}\Python\Scripts\theonionpack.exe"; \ + WorkingDir: "{app}"; \ + Flags: runminimized; \ + Parameters: "--tor ""{app}\Tor"""; \ + Comment: "Launching The Onion Pack..."; + +[CustomMessages] +MSG_INSTALLING_TOP=Now installing The Onion Pack. This may take some time, as a number of additional packages most probably have to be collected from the Internet... +MSG_FAILED_PIP=Unfortunately we were not able to orderly setup the Python environment. +MSG_FAILED_TOB=We failed to install the necessary packages for The Onion Pack into our Python environment. +MSG_FAILED_TOP=We failed to add The Onion Pack to the Python environment. +MSG_FAILED_FINISHED=Setup failed to install The Onion Pack on your computer. You may run the uninstaller to remove now the obsolete remainders of this procedure. Sorry for this inconvenience! + + +[Run] +; Those runners check - parameter AfterInstall - if a dedicated file (that was part of the current step of installation) exists. +; If not, ConfirmInstallation raises a MsgBox and sets the error flag - to abort installation. +; From step two on, ConfirmNoInstallError (parameter Check) confirms that the error flag is down. If raised, this step is skipped. + +; We start by getting pip. +Filename: "{app}\Python\python.exe"; \ + Parameters: "get-pip.py ""pip>18"" --no-warn-script-location"; \ + Flags: runhidden; \ + StatusMsg: "Preparing the Python runtime environment..."; \ + BeforeInstall: SetupRunConfig; \ + AfterInstall: ConfirmInstallation('pip.exe', ExpandConstant('{cm:MSG_FAILED_PIP}')) +; +; We pip theonionbox as individual package - despite it's as well defined as dependency for theonionpack. +; This ensures that we can upgrade to the latest tob by simply re-running this (unmodified) installer. +; We can pip from a local package using /tob! +Filename: "{app}\Python\python.exe"; \ + Parameters: "{code:create_pip_command|{param:tob|theonionbox}}"; \ + Flags: runhidden; \ + StatusMsg: {cm:MSG_INSTALLING_TOP}; \ + Check: ConfirmNoInstallError; \ + BeforeInstall: SetupRunConfig; \ + AfterInstall: ConfirmInstallation('theonionbox.exe', ExpandConstant('{cm:MSG_FAILED_TOB}')) +; +;The next line implements command line parameter /top (e.g. /top="theonionpack.tar.gz") to pip from a local package. +;There are 2 scenarios - depending on the COMPILER command line parameter 'theonionpack' +; #1) This installer carries a (default) package - thus #ifdef theonionpack: +; In this case, the local package will be installed, if it exists. If not, we'll install the default package. +; #2) This installer carries NO (default) package (#ifndef theonionpack): +; Then we'll pip the local package, if it exists. If not, we'll try to pip from PyPI. +#ifdef theonionpack + Filename: "{app}\Python\python.exe"; \ + Parameters: "{code:create_pip_command|{param:top|{#theonionpack}}}"; \ + Flags: runhidden; \ + StatusMsg: {cm:MSG_INSTALLING_TOP}; \ + Check: ConfirmNoInstallError; \ + BeforeInstall: SetupRunConfig; \ + AfterInstall: ConfirmInstallation('theonionpack.exe', ExpandConstant('{cm:MSG_FAILED_TOP}')) +#else + Filename: "{app}\Python\python.exe"; \ + Parameters: "{code:create_pip_command|{param:top|theonionpack}}"; \ + Flags: runhidden; \ + StatusMsg: {cm:MSG_INSTALLING_TOP}; \ + Check: ConfirmNoInstallError; \ + BeforeInstall: SetupRunConfig; \ + AfterInstall: ConfirmInstallation('theonionpack.exe', ExpandConstant('{cm:MSG_FAILED_TOP}')) +#endif + +; We offer 'Run The Onion Pack...' if there was no install error. +Filename: "{app}\The Onion Pack.lnk"; \ + WorkingDir: "{app}"; \ + Flags: postinstall shellexec; \ + Description: "Run The Onion Pack..."; \ + Verb: "open"; \ + Check: ConfirmNoInstallError; + +; Alternatively, we propose 'Run Uninstaller...' if an error occured! +Filename: "{uninstallexe}"; \ + Flags: postinstall shellexec; \ + Description: "Run Uninstaller..."; \ + Verb: "open"; \ + Check: IfInstallationError; + + +[InstallDelete] +; InstallDelete ... deletes files as the first step of installation!! +; Thus it's of no use for us! + + +[UninstallRun] +; To uninstall, we freeze the Python environment and write the names of the currently installed packages +; into a dedicated file (unins.req). +Filename: "{cmd}"; Parameters: """{cmd}"" /S /C """"{app}\Python\Scripts\pip.exe"" freeze > ""{app}\unins.req"""""; Flags: runhidden +; This done, we ask pip to remove all those packages. +Filename: "{cmd}"; Parameters: """{cmd}"" /S /C """"{app}\Python\Scripts\pip.exe"" uninstall -y -r ""{app}\unins.req"""""; Flags: runhidden +; Finally pip may remove itself & it's friends. +Filename: "{app}\Python\python.exe"; Parameters: "-m pip uninstall -y pip setuptools wheel"; Flags: runhidden + + +[UninstallDelete] +; Housekeeping... +Type: files; Name: "{app}\unins.req" +Type: dirifempty; Name: "{app}\Python\Lib\site-packages" +Type: dirifempty; Name: "{app}\Python\Lib" +Type: dirifempty; Name: "{app}\Python\service" +; Type: dirifempty; Name: "{app}\Python\support\osxtemp" +; Type: dirifempty; Name: "{app}\Python\support" +; Type: dirifempty; Name: "{app}\Python\theonionbox\tob\system\windows" +; Type: dirifempty; Name: "{app}\Python\theonionbox\tob\system" +; Type: dirifempty; Name: "{app}\Python\theonionbox\tob" +; Type: dirifempty; Name: "{app}\Python\theonionbox" +;Type: files; Name: "{app}\Tor\Data\torrc-defaults" + +[Tasks] +Name: "startup"; Description: "Start The Onion Pack when you start Windows"; GroupDescription: "Autostart" + +[Code] +var + // Custom page showing progress while extracting the Tor Download Link + TorDownloadLinkPage: TOutputProgressWizardPage; + + // Independence Statement Acknowledgement Page + IndependencePage: TOutputMsgMemoWizardPage; + IndependenceAcceptedRadio: TRadioButton; + IndependenceNotAcceptedRadio: TRadioButton; + + // This is the application wide Error Flag. + error: Boolean; + +procedure CreateIndependencePage(); forward; +procedure CheckIndependenceAccepted(Sender: TObject); forward; + +procedure debug(message: string); +begin + Log('[TOP] ' + message); +end; + +procedure InitializeWizard(); +begin + + // We are going to download Python from python.org... + // ... and get-pip.py from pypa,io. + + // the target file shall end with '.zip' ... to later support unzipping! + idpAddFile('https://www.python.org/ftp/python/{#py}/python-{#py}-embed-win32.zip', ExpandConstant('{tmp}\python.zip')); + idpAddFile('https://bootstrap.pypa.io/get-pip.py', ExpandConstant('{tmp}\get-pip.py')); + + // Yet we'll do this later - after the preparation stage. + idpDownloadAfter(wpPreparing); + + // Initialize the custom page to fetch the Tor Doenload link. + // This Link (if found) will later (@ PrepareToInstall) be added + // to the files to be downloaded => becoming {tmp}\tor.zip + TorDownloadLinkPage:= CreateOutputProgressPage('Extracting Download Link for current Tor version', ''); + + // Create the page to acknowledge the Statement of Independence + CreateIndependencePage(); + +end; + + +// pastebin.com/STcQLfKR +Function SplitString(const Value: string; Delimiter: string; Strings: TStrings): Boolean; +var + S: string; +begin + S := Value; + if StringChangeEx(S, Delimiter, #13#10, True) > 0 then begin + Strings.text := S; + Result := True; + Exit; + end; + Result := False; +end; + + +// To unzip a file; based on an answer by willw @ 20160107 +// https://stackoverflow.com/questions/6065364/how-to-get-inno-setup-to-unzip-a-file-it-installed-all-as-part-of-the-one-insta +const + SHCONTCH_NOPROGRESSBOX = 4; + SHCONTCH_RESPONDYESTOALL = 16; + +procedure UnZip(ZipPath, TargetPath: string); +var + Shell: Variant; + ZipFile: Variant; + TargetFolder: Variant; +begin + debug('Unzipping ' + ZipPath + ' -> ' + TargetPath); + + Shell := CreateOleObject('Shell.Application'); + + ZipFile := Shell.NameSpace(ZipPath); + if VarIsClear(ZipFile) then + RaiseException(Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath])); + + TargetFolder := Shell.NameSpace(TargetPath); + if VarIsClear(TargetFolder) then + if CreateDir(TargetPath) <> True then + RaiseException(Format('Target path "%s" does not exist', [TargetPath])) + else + TargetFolder := Shell.NameSpace(TargetPath); + + TargetFolder.CopyHere(ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL); + +end; + + +// Extract the Tor Package Download Link +// html: array of string, each string representing one line of the source code of www.torproject.org/download/tor +// pbar: reference to the progressbar on the wizards page - to provide feedback +// Result: Download Link (if found), otherwize '' +function ExtractDownloadLink(const html: array of string; const pbar: TOutputProgressWizardPage): string; +var + line, tag, address: string; + linesplit, tagsplit: TStringList; + i, ii, iii: Integer; + +begin + + debug('Trying to fetch Tor download link...'); + + Result:= ''; + + for i := 0 to GetArrayLength(html) - 1 do begin + line := html[i]; + pbar.SetProgress(pbar.ProgressBar.Position + 1, pbar.ProgressBar.Max); + // find line with "class='downloadLink'" + if StringChangeEx(line, 'downloadLink', 'found', True) > 0 then begin + // split this line @ ' ' + linesplit := TStringList.Create; + if SplitString(line, ' ', linesplit) = True then begin + for ii := 0 to linesplit.Count - 1 do begin + // find a tag that has a 'zip' in it + tag := linesplit.Strings[ii]; + if StringChangeEx(tag, 'zip', 'xxx', True) > 0 then begin + // split this tag @ '"' ... to extract the address portion + tagsplit := TStringList.Create; + if SplitString(tag, '"', tagsplit) = True then begin + for iii := 0 to tagsplit.Count - 1 do begin + // check if it's in a expected format + // ToDo: add more checks? + address := tagsplit.Strings[iii]; + if Length(address) > 3 then begin + if Copy(address, Length(address) - 2, 3) = 'xxx' then begin + // convert back to original; found! + StringChangeEx(address, 'xxx', 'zip', True); + Result := address; + Break; + end; + end; + end; + end; + tagsplit.Free(); + end; + end; + end; + linesplit.Free(); + end; + end; + + debug('Tor Download Link: ' + Result); +end; + + +procedure CurStepChanged(CurStep: TSetupStep); + +var + pth: String; + +begin + if CurStep = ssInstall then begin + // Unzip downloaded files; will be copied by Inno to the target directory + // Thus we support propper uninstalling later. + UnZip(ExpandConstant('{tmp}\tor.zip'), ExpandConstant('{tmp}\Tor')); + UnZip(ExpandConstant('{tmp}\python.zip'), ExpandConstant('{tmp}\Python')); + + // Patch Python ... + // Mandatory to enable pip operations later! + pth := ExpandConstant('{tmp}\Python\python{#pth}._pth'); + SaveStringsToFile(pth, ['', '# by TheOnionPack', '.\Lib\site-packages', 'import site'], true); + + end; + if CurStep = ssPostInstall then + begin + end; +end; + + +procedure CurPageChanged(CurPageID: Integer); +begin + + // Update Next button when user gets to second license page + if CurPageID = IndependencePage.ID then + begin + CheckIndependenceAccepted(nil); + end; + + // Customize FinishedPage in case of error. + if CurPageID = wpFinished then begin + if error = True then begin + WizardForm.FinishedHeadingLabel.Caption := 'The Onion Pack Setup Error'; + WizardForm.FinishedLabel.Caption := ExpandConstant('{cm:MSG_FAILED_FINISHED}'); + end; + end; +end; + + +function PrepareToInstall(var NeedsRestart: Boolean): String; +var + html: array of string; + tor, link: string; + check: Boolean; + +begin + + // Extract the Tor Download Link from the Tor Website + // This serves as well to verify that an internet connection is present. + + TorDownloadLinkPage.SetText('Fetching Tor Download Webpage...', ''); + TorDownloadLinkPage.ProgressBar.Style := npbstNormal; + TorDownloadLinkPage.SetProgress(1, 2); + TorDownloadLinkPage.Show; + + // Give the page time to setup nicely + // Note: This does not work! :( + Sleep(500); + + try + // Download the website + check := idpDownloadFile('{#tor}', ExpandConstant('{tmp}\tor.check')); + if check = False then begin + // if this failed, we cannot continue. + Result := 'Failed to fetch the Tor Download Webpage @ {#tor}' + #13#10 + + #13#10 +'Please verify that you''re connected to the Internet.'; + WizardForm.PreparingLabel.WordWrap := True; + Exit; + end; + + // Now extract the link + TorDownloadLinkPage.SetText('Extracting Download Link...', ''); + LoadStringsFromFile(ExpandConstant('{tmp}\tor.check'), html); + TorDownloadLinkPage.ProgressBar.Style := npbstNormal; + TorDownloadLinkPage.SetProgress(2, GetArrayLength(html)+1); + link := ExtractDownloadLink(html, TorDownloadLinkPage); + // This is a very (very very) simplistic check that we found it. + if Length(link) < 3 then begin + // if not: ... we've got a problem! + Result := 'Failed to extract the Tor Download Link from ' + tor + #13#10 + #13#10 + + 'Most probably the page layout has been altered recently.'#13#10 + + 'Please ckeck for an updated version of The Onion Pack or raise an issue at our GitHub page.'; + WizardForm.PreparingLabel.WordWrap := True; + // ToDo: Add link to github & a procedure: + // https://stackoverflow.com/questions/38934332/how-can-i-make-a-button-or-a-text-in-inno-setup-that-opens-web-page-when-clicked + Exit; + end else begin + // We have a link! Let's append it to the download queue: + idpAddFile('https://www.torproject.org' + link, ExpandConstant('{tmp}\tor.zip')); + + end; + finally + TorDownloadLinkPage.Hide; + end; +end; + + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then begin + end; +end; + + +// Used while installing the python packages +// https://stackoverflow.com/questions/34336466/inno-setup-how-to-manipulate-progress-bar-on-run-section +procedure SetMarqueeProgress(Marquee: Boolean); +begin + if Marquee then begin + WizardForm.ProgressGauge.Style := npbstMarquee; + end else begin + WizardForm.ProgressGauge.Style := npbstNormal; + end; +end; + + +// To be used for the lengthy status messages in the [Run] section +procedure SetupRunConfig(); +begin + SetMarqueeProgress(True); + + WizardForm.StatusLabel.WordWrap := True; + WizardForm.StatusLabel.AdjustHeight(); +end; + + +// Additional page to acknowledge Statement of Independence +// https://stackoverflow.com/questions/34592002/how-to-create-two-licensefile-pages-in-inno-setup +procedure CheckIndependenceAccepted(Sender: TObject); +begin + // Update Next button when user (un)accepts the license + WizardForm.NextButton.Enabled := IndependenceAcceptedRadio.Checked; +end; + +function CloneLicenseRadioButton(Source: TRadioButton): TRadioButton; +begin + Result := TRadioButton.Create(WizardForm); + Result.Parent := IndependencePage.Surface; + Result.Caption := Source.Caption; + Result.Left := Source.Left; + Result.Top := Source.Top; + Result.Width := Source.Width; + Result.Height := Source.Height; + Result.OnClick := @CheckIndependenceAccepted; +end; + +procedure CreateIndependencePage(); +var + IndependenceFileName: string; + IndependenceFilePath: string; + +begin + + IndependencePage := + CreateOutputMsgMemoPage( + wpLicense, 'Statement of Independence', SetupMessage(msgLicenseLabel), + 'Please read the following Statement of Independence. You must ' + + 'acknowledge this statement before continuing with the installation.', ''); + + // Shrink memo box to make space for radio buttons + IndependencePage.RichEditViewer.Height := WizardForm.LicenseMemo.Height; + + // Load SoI + // Loading ex-post, as Lines.LoadFromFile supports UTF-8, + // contrary to LoadStringFromFile. + IndependenceFileName := 'INDEPENDENCE'; + ExtractTemporaryFile(IndependenceFileName); + IndependenceFilePath := ExpandConstant('{tmp}\' + IndependenceFileName); + IndependencePage.RichEditViewer.Lines.LoadFromFile(IndependenceFilePath); + DeleteFile(IndependenceFilePath); + + // Clone accept/do not accept radio buttons for the second license + IndependenceAcceptedRadio := + CloneLicenseRadioButton(WizardForm.LicenseAcceptedRadio); + IndependenceNotAcceptedRadio := + CloneLicenseRadioButton(WizardForm.LicenseNotAcceptedRadio); + + // Customize captions + IndependenceAcceptedRadio.Caption := 'I acknowledge this statement.' + IndependenceNotAcceptedRadio.Caption := 'I do not acknowledge this statement.' + + // Initially not accepted + IndependenceNotAcceptedRadio.Checked := True; + +end; + +// To get the absolute path for any source file +function GetAbsSourcePath(const path: string): string; +var + abs_path: string; + +begin + // check if path is absolute + abs_path := ExpandFileName(path); + if abs_path = path then begin + Result := path; + end else begin + // if not: generate the absolute one. + Result := ExpandConstant('{src}\' + path); + end; + debug('AbsPath for ' + path + ' -> ' + Result); +end; + + +function CheckIfExists(const FileName: string): Boolean; +var + abs_path: string; +begin + abs_path := GetAbsSourcePath(FileName); + Result:=FileExists(abs_path); + debug('Does ' + FileName + ' exist? -> ' + IntToStr(Integer(Result))); +end; + +function create_pip_command(const path: string): string; +var + r: string; +begin + r := '-m pip install --no-warn-script-location --upgrade ""'; + r := r + ExtractFileName(path); + Result := r + '""'; + debug('pip command: ' + Result); +end; + +function ExtractFN(const path: string): string; +begin + Result:= ExtractFileName(path); + debug('FN of ' + path + ' -> ' + Result); +end; + +// verify that filename exists. If not, emit MsgBox & set Error Flag. +procedure ConfirmInstallation(filename: string; msg: string); +begin + if FileExists(ExpandConstant('{app}\Python\Scripts\' + filename)) = False then begin + SetMarqueeProgress(False); + TaskDialogMsgBox('Error', + msg, + mbCriticalError, + MB_OK, [], 0); + error := True; + debug(filename + ' does NOT exist!'); + end else begin + debug(filename + ' exist!'); + end; +end; + +// Error Flag verification +function ConfirmNoInstallError(): Boolean; +begin + Result := (error = False); + debug('NoInstallError: ' + IntToStr(Integer(Result))); +end; + +function IfInstallationError(): Boolean; +begin + Result := (error = True); + debug('InstallationError: ' + IntToStr(Integer(Result))); +end; diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d2b776c --- /dev/null +++ b/setup.py @@ -0,0 +1,369 @@ +from setuptools import setup +import os +import sys +import fnmatch +import setuptools.command.build_ext +import setuptools.command.sdist +import setuptools.command.install +# from theonionbox.stamp import __version__, __description__ + +import configparser + +config = configparser.ConfigParser() +config.read('theonionpack/setup.ini') +__version__ = config['theonionpack']['version'] +__description__ = config['theonionpack']['description'] + +from distutils.extension import Extension + +# Custom command to compile the latest README.html +# BTW: grip is quite cool! +def CompileREADME(): + + # https://stackoverflow.com/questions/3431825/generating-an-md5-checksum-of-a-file + def get_hash(filename): + + import hashlib + + def hash_bytestr_iter(bytesiter, hasher, ashexstr=False): + for block in bytesiter: + hasher.update(block) + return (hasher.hexdigest() if ashexstr else hasher.digest()) + + def file_as_blockiter(afile, blocksize=65536): + with afile: + block = afile.read(blocksize) + while len(block) > 0: + yield block + block = afile.read(blocksize) + + return hash_bytestr_iter(file_as_blockiter(open(filename, 'rb')), hashlib.sha256(), True) + + # tor.1.txt production + try: + from xtor import TorTxt + tt = TorTxt(force=False) + if tt.run() is True: + tt.copy(os.path.join('theonionbox','tor')) + except: + pass + + old_md_hash = '' + old_html_hash = '' + old_rst_hash = '' + + current_md_hash = 'doit' + current_html_hash = 'doit' + current_rst_hash = 'doit' + + try: + with open('readme/README.hash', 'r') as f: + lines = f.readlines() + if len(lines) == 3: + old_md_hash = lines[0].strip() + old_html_hash = lines[1].strip() + old_rst_hash = lines[2].strip() + except Exception as e: + # raise e + pass + + try: + current_md_hash = get_hash('README.md') + current_html_hash = get_hash('readme/README.html') + current_rst_hash = get_hash('readme/README.rst') + except Exception as e: + # raise e + pass + + hash_changed = False + + if (old_md_hash != current_md_hash) or (old_html_hash != current_html_hash): + from grip import export + export(path='README.md', out_filename='readme/README.html', title='The Onion Box v{}'.format(__version__)) + hash_changed = True + else: + print('Skiping generation of README.html; files unchanged!') + + do_rst = False + if do_rst is True: + if (old_md_hash != current_md_hash) or (old_rst_hash != current_rst_hash): + # path defined by: brew install pandoc + # os.environ.setdefault('PYPANDOC_PANDOC', '/usr/local/Cellar/pandoc/2.1') + from pypandoc import convert_file + print('Generating README.rst') + convert_file('README.md', 'rst', outputfile="readme/README.rst") + hash_changed = True + else: + print('Skiping generation of README.rst; files unchanged!') + else: + print('Generation of README.rst intentionally deactivated!') + + if hash_changed is True: + with open('readme/README.hash', 'w') as f: + f.write(current_md_hash+'\n'+current_html_hash+'\n'+current_rst_hash) + + +# class PostInstallCommand(setuptools.command.install.install): +# +# def run(self): +# import os +# # start with standard staff... +# setuptools.command.install.install.run(self) +# +# # post install activities +# # http://stackoverflow.com/a/1883251/1286571 +# import sys +# if hasattr(sys, 'real_prefix'): +# os.chmod('theonionbox/theonionbox/run.sh', int('755', 8)) +# else: +# print("No real_prefix") + + +# def CompileOSXTemp(): +# +# from subprocess import call +# from Cython.Build import cythonize +# from setuptools.extension import Extension +# +# osxtemp_path = 'support/osxtemp' +# libsmc_path = os.path.join(osxtemp_path, 'libsmc') +# +# libsmc_clean = ['make', +# '--directory={}'.format(libsmc_path), +# 'clean'] +# +# libsmc_cmd = ['make', +# '--directory={}'.format(libsmc_path), +# 'dynamic'] +# +# print("Compiling '{}'".format(libsmc_path)) +# # call(libsmc_clean) +# call(libsmc_cmd) +# +# pth = os.path.join(osxtemp_path, '*.pyx') +# osxt = cythonize(pth, force=True) +# +# print(osxt) +# pass +# pass + + +# Linking custom command into the sdist chain +# https://seasonofcode.com/posts/how-to-add-custom-build-steps-and-commands-to-setuppy.html +class sdist(setuptools.command.sdist.sdist): + + def run(self): + + # CompileOSXTemp() + # CompileREADME() + + # continue with standard staff... + setuptools.command.sdist.sdist.run(self) + + +def generate_package_data(package_data, package_dir=None): + """ + :param package_data: package_data as expected by setup.py, recursive dir wildcards + :type package_data: dict + :param package_dir: package_dir as expected by setup.py + :type package_dir: dict + :return: package_data as expected by setup.py, recursive directories expanded + :rtype: dict + """ + + out = {} + + package_dir = package_dir or {} + + for key, paths in package_data.items(): + out_path = [] + + base_path = package_dir[key] if key in package_dir else '' + + for path_item in paths: + root = os.path.join(base_path, path_item) + + if os.path.isfile(root): + out_path.append(path_item) + continue + + root_dir, root_file = os.path.split(root) + + for (dirpath, dirnames, filenames) in os.walk(root_dir): + out_path.append(os.path.relpath(os.path.join(dirpath, root_file), base_path)) + + out[key] = out_path + + return out + + +def generate_data_files(data_files): + """ + :param data_files: data_files as expected by setup.py, recursive dir wildcards + :type package_data: list + :return: data_files as expected by setup.py, recursive files expanded + :rtype: list + """ + + out = {} + + for target, sources in data_files: + + if target not in out: + out[target] = [] + + for source in sources: + + if os.path.isfile(source): + out[target].append(source) + continue + + source_dir, source_match = os.path.split(source) + + for (dirpath, dirnames, filenames) in os.walk(source_dir): + for file in filenames: + if fnmatch.fnmatch(file, source_match): + file_rel_target = os.path.relpath(dirpath, source_dir) + file_target = os.path.join(target, file_rel_target) + + if file_target not in out: + out[file_target] = [] + + out[file_target].append(os.path.join(dirpath, file)) + + retval = [] + for key, items in out.items(): + retval.append((key, items)) + + return retval + + +packages = [ + 'theonionpack', + 'theonionpack.top' +] + +package_dir = { + 'theonionpack': 'theonionpack', + 'theonionpack.top': 'theonionpack/top' +} + +package_data = { + # 'theonionbox': ['config/*', + # 'css/*', + # 'font/*', + # 'libs/*', + # 'pages/*', + # 'scripts/*', + # 'sections/*', + # 'tor/*', + # 'uptime/*', + # ] + 'theonionpack': ['icons/top16.ico' + , 'setup.ini' + , '../README.md' + ] +} + +data_files = [ + # ('docs', ['docs/*.*']), + # ('', ['readme/README.html']), + # ('config', ['theonionbox/config/*.*']), + # ('service', []), + # ('service/FreeBSD', ['FreeBSD/theonionbox.sh']), + # ('service/init.d', ['init.d/theonionbox.sh']), + # ('service/systemd', ['systemd/theonionbox.service']), + # ('support', []), + # ('support/osxtemp', []), + # ('support/osxtemp/libsmc', ['support/osxtemp/libsmc/LICENSE', 'support/osxtemp/libsmc/Makefile']), + # ('support/osxtemp/libsmc/include', ['support/osxtemp/libsmc/include/smc.h']), + # ('support/osxtemp/libsmc/src', ['support/osxtemp/libsmc/src/smc.c']), +] +# print(generate_data_files(data_files)) + +# import platform +# +# def extensions(system=platform.system()): +# +# run_cythonize = False +# try: +# from Cython.Build import cythonize +# run_cythonize = True +# except ImportError: +# pass +# +# ext = [] +# if system == 'Darwin' and True is False: # disabled 20180417 +# +# # 'osxtemp' +# path = 'support/osxtemp' +# +# sf = '*.pyx' if run_cythonize is True else '*.c' +# source_files = os.path.join(path, sf) +# +# # path to the libsmc library +# libsmc_path = os.path.join(path, 'libsmc') +# +# ext.append(Extension(name='theonionbox.tob.osxtemp', +# include_dirs=[path, libsmc_path], +# depends=[os.path.join(libsmc_path, 'include', 'smc.h')], +# sources=[source_files, +# os.path.join(libsmc_path, 'src', 'smc.c')] +# ) +# ) +# +# if run_cythonize is True: +# try: +# ext = cythonize(ext) +# except: +# ext = [] +# +# return ext + + +setup( + cmdclass={'sdist': sdist, + }, + name='theonionpack', + version=__version__, + # py_modules=['theonionbox.py'], + packages=packages, + package_dir=package_dir, + package_data=generate_package_data(package_data, package_dir), + data_files=generate_data_files(data_files), + url='https://github.com/ralphwetzel/theonionpack', + license='MIT', + author='Ralph Wetzel', + author_email='theonionbox@gmx.com', + description=__description__, + # long_description=open('docs/description.rst').read(), + entry_points={ + 'console_scripts': [ + 'theonionpack = theonionpack.__main__:main'] + }, + install_requires=[ + 'theonionbox>=20.1', + 'pystray', + 'shelljob', + 'filelock' + ], + long_description_content_type='text/x-rst; charset=UTF-8', + classifiers=[ + # https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Environment :: Web Environment', + 'Framework :: Bottle', + 'Intended Audience :: End Users/Desktop', + 'Intended Audience :: Information Technology', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Topic :: System :: Networking :: Monitoring', + 'Topic :: Utilities', + ], + platforms=['Windows'], + # ext_modules=extensions(), +) diff --git a/theonionpack/__init__.py b/theonionpack/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/theonionpack/__init__.py @@ -0,0 +1 @@ + diff --git a/theonionpack/__main__.py b/theonionpack/__main__.py new file mode 100644 index 0000000..15213f2 --- /dev/null +++ b/theonionpack/__main__.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +import importlib +import pathlib +import site + + +def main(): + + # Per definition, __main__.__file__ is the only __file__, that could carry a relative path! + # => https://docs.python.org/3.4/whatsnew/3.4.html#other-language-changes + # So we resolve it here! + path = pathlib.Path(__file__).resolve() + + # If run as 'python -m xy', '.' is not part of sys.path. + # __package__ as well is either '' or None. + # Therefore any import from our package fails. + # Solution: Add the path of __main__.py (this file) to sys.path + if __name__ == '__main__' and __package__ in ['', None]: + site.addsitedir(str(path.parent)) + from theonionpack import main as packmain + else: + from .theonionpack import main as packmain + + packmain() + + +if __name__ == '__main__': + main() diff --git a/theonionpack/icons/top16.ico b/theonionpack/icons/top16.ico new file mode 100644 index 0000000..373c71e Binary files /dev/null and b/theonionpack/icons/top16.ico differ diff --git a/theonionpack/setup.ini b/theonionpack/setup.ini new file mode 100644 index 0000000..702a708 --- /dev/null +++ b/theonionpack/setup.ini @@ -0,0 +1,12 @@ +[theonionpack] +title=The Onion Pack +description=Tor Relay Bundle for Windows +version=20.1 +copyright=2019 - 2020 Ralph Wetzel + +[python] +version=3.7.6 + +[tor] +download=https://www.torproject.org/download/tor/ + diff --git a/theonionpack/theonionpack.py b/theonionpack/theonionpack.py new file mode 100644 index 0000000..deed771 --- /dev/null +++ b/theonionpack/theonionpack.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +import click +import pathlib +import site + +# theonionpack.py -d/-t --tor --data + +import configparser + +cwd = pathlib.Path(__file__).resolve() +cwd = cwd.parent +assert cwd.exists() + +config = configparser.ConfigParser() +config.read(str(cwd / 'setup.ini')) +__title__ = config['theonionpack']['title'] +__version__ = config['theonionpack']['version'] +__description__ = config['theonionpack']['description'] + + +# @click.group(chain=True, invoke_without_command=True) +@click.command() +@click.option('--debug', is_flag=True, flag_value=True, + help='Switch on DEBUG mode.') +@click.option('--trace', is_flag=True, flag_value=True, + help='Switch on TRACE mode (which is more verbose than DEBUG mode).') +@click.option('-t', '--tor', default='.\Tor', show_default=True, + type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, + resolve_path=True, allow_dash=False), + help="Search directory for 'tor.exe'.") +@click.option('-d', '--data', default='.\Data', show_default=True, + type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, + resolve_path=True, allow_dash=False), + help="Tor's DataDirectory.") +@click.version_option(prog_name=f'{__title__}: {__description__}', + version=__version__, message='%(prog)s\nVersion %(version)s') + +@click.pass_context +def main(ctx, debug, trace, tor, data): + + params = { + 'debug': debug, + 'trace': trace, + 'tor': tor, + 'data': data, + 'cwd': str(cwd) + } + + if __name__ == '__main__' or __package__ in [None, '']: + site.addsitedir(str(cwd)) + from top.pack import Pack + else: + from .top.pack import Pack + + top = Pack(params) + top.run() + + +if __name__ == '__main__': + main() + + +__all__ = ['main'] diff --git a/theonionpack/top/__init__.py b/theonionpack/top/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/theonionpack/top/box.py b/theonionpack/top/box.py new file mode 100644 index 0000000..89c19c5 --- /dev/null +++ b/theonionpack/top/box.py @@ -0,0 +1,57 @@ +import importlib +import os +import pathlib +import signal +import sys +import subprocess + +from .util import MBox + + +class TheOnionBox(): + + def __init__(self, config): + + self.config = config + + tob = importlib.util.find_spec('theonionbox') + if tob is None: + MBox("Error: Failed to locate Python package 'theonionbox'.", style=0x10) + sys.exit(0) + + self.name = tob.name + + self.tob = None + self.password = None + + def run(self, password: str = None): + + params = [sys.executable, '-m', self.name] + + if self.config['trace']: + params.extend(['--trace']) + elif self.config['debug']: + params.extend(['--debug']) + + params.extend(['box', '--host', '127.0.0.1']) + if password is not None: + params.extend(['tor', '--password', password]) + self.password = password + + if self.config['trace'] or self.config['debug']: + self.tob = subprocess.Popen(params, creationflags=subprocess.CREATE_NEW_CONSOLE) + else: + self.tob = subprocess.Popen(params) + + return self.tob + + def stop(self): + # if the subprocess is still running... + if self.poll() is None: + # ... terminate it. + pid = self.tob.pid + os.kill(pid, signal.SIGINT) + self.tob.wait() + + def poll(self): + return self.tob.poll() \ No newline at end of file diff --git a/theonionpack/top/pack.py b/theonionpack/top/pack.py new file mode 100644 index 0000000..ddde6f9 --- /dev/null +++ b/theonionpack/top/pack.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python +import ctypes +import os +import pathlib +import subprocess +import sys +import tempfile +from time import sleep +import threading +import uuid +import webbrowser +import winreg + +from filelock import FileLock, Timeout +from PIL import Image +import pystray + +from . import tor +from . import box +from .simplecontroller import SimplePort +from .util import MBox + +# Hide the console... +kernel32 = ctypes.WinDLL('kernel32') +user32 = ctypes.WinDLL('user32') +SW_HIDE = 0 +hWnd = kernel32.GetConsoleWindow() +if hWnd: + user32.ShowWindow(hWnd, SW_HIDE) + + +class Pack(): + + def __init__(self, config): + self.config = config + + self.cwd = pathlib.WindowsPath(self.config['cwd']) + + self.status = 0 + + # Prepare Tor + self.password = uuid.uuid4().hex + self.relay = tor.Tor(self.config['tor'], self.config['data']) + + # torrc + torrc = pathlib.Path(config['data']) / 'torrc' / 'torrc' + self.torrc = torrc.resolve() + + # The Onion Box + self.box = box.TheOnionBox(config) + + # Stop signal, to terminate our run_loop + self.stop = threading.Event() + + # the Tray icon + self.tray = pystray.Icon('theonionpack', title='The Onion Pack') + + self.tray.icon = Image.open(str(self.cwd / 'icons' / 'top16.ico')) + + self.tray.menu = pystray.Menu( + pystray.MenuItem( + 'Monitor...', + action=self.on_monitor, + default=True + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + 'Relay Control', + pystray.Menu( + pystray.MenuItem( + 'Edit configuration file...', + action=self.on_open_torrc + ), + pystray.MenuItem( + 'Show logfile...', + action=self.on_show_messages + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + 'Reload relay configuration', + action=self.on_reload_config + ) + ) + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + 'Stop!', + action=self.on_quit + ) + ) + + def run(self): + + self.lock = FileLock(str(self.cwd / 'theonionpack.lock')) + running = False + + try: + with self.lock.acquire(timeout=0): + + # run The Onion Box + tob = self.box.run(password=self.password) + + # run Tor + self.relay.run(password=self.password) + + running = True + + # run the Tray icon + # !! This is a blocking call !! + self.tray.run(self.run_loop) + + # the block may be released by self.on_quit, issued by an operator via the Tray + # ... or by a system command (like SIGTERM). + + except Timeout: + MBox("It seems like another instance of The Onion Pack is already running. Aborting launch procedure...", + style=0x10) + + finally: + self.lock.release() + + if running: + + # Stop theonionbox + self.box.stop() + + # Tor has OwningControllerProcess defined ... thus will terminate as soon as we're done. + + if self.status == 1: + MBox("Our instance of TheOnionBox terminated.\r\nThus we have to terminate as well! Sorry...", + style=0x10) + + sys.exit(0) + + def run_loop(self, icon: pystray.Icon): + + icon.visible = True + + while self.stop.is_set() is False: + + # quit if TheOnionBox died! + if self.box.poll() is not None: + + # indicate that the Box terminated! + self.status += 1 + self.do_quit() + return + + self.relay.collect_messages() + sleep(1) + + # Tray menu actions + def on_monitor(self, icon, item): + webbrowser.open_new_tab('http://127.0.0.1:8080/') + + def on_quit(self, icon, item): + self.do_quit() + + def do_quit(self): + + # Stop the run_loop + self.stop.set() + + # Stop the Tray + self.tray.stop() + + # cleanup is being performed in self.run() + + # def get_tor_messages(self): + # while True: + # self.relay.collect_messages() + # sleep(5) + + def on_show_messages(self, icon, item): + fd, name = tempfile.mkstemp(prefix="Tor_", suffix='.html', text=True) + with open(fd, 'w') as tmp: + tmp.write('
'.join(self.relay.messages)) + webbrowser.open_new_tab(name) + + def on_open_torrc(self): + + def get_default_windows_app(suffix): + + class_root = winreg.QueryValue(winreg.HKEY_CLASSES_ROOT, suffix) + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r'{}\shell\open\command'.format(class_root)) as key: + command = winreg.QueryValueEx(key, '')[0] + return command.split(' ')[0] + + if not self.torrc.exists(): + self.torrc.parent.mkdir(parents=True, exist_ok=True) + self.torrc.touch() + + path = get_default_windows_app('.txt') + subprocess.Popen([os.path.expandvars(path), str(self.torrc)]) + + def on_reload_config(self): + + controller = None + try: + controller = SimplePort('127.0.0.1', 9051) + except Exception: + MBox('Failed to connect to the local Tor relay.', style=0x10) + + if controller is None: + return + + ok = '' + try: + ok = controller.msg(f'AUTHENTICATE "{self.password}"') + except: + if ok != '250 OK': + MBox('Failed to authenticate against local Tor relay.', style=0x10) + controller.shutdown() + return + + ok = '' + try: + ok = controller.msg("SIGNAL RELOAD") + except: + if ok != '250 OK': + MBox('Failed to reload the Tor relay configuration.', style=0x10) + + controller.shutdown() + return diff --git a/theonionpack/top/simplecontroller.py b/theonionpack/top/simplecontroller.py new file mode 100644 index 0000000..f13aa9c --- /dev/null +++ b/theonionpack/top/simplecontroller.py @@ -0,0 +1,87 @@ +import threading +# from socks import socksocket +from socket import socket, AF_INET, SOCK_STREAM + +# "Thanks" to stem! +try: + # Added in 3.x + import queue +except ImportError: + import Queue as queue + + +class SimpleController(object): + + _socket = None + + def __init__(self): + + assert (self._socket is not None), 'SimpleController is not initialized!' + + self.msg_queue = queue.Queue() + self._is_alive = True + self._msg_lock = threading.RLock() + + self._reader_thread = threading.Thread(target=self._reader, name='TOB') + self._reader_thread.setDaemon(True) + self._reader_thread.start() + + def _reader(self): + while self._is_alive: + try: + control_message = self._socket.recv(4096) + self.msg_queue.put(control_message) + except Exception as exc: + pass + + def shutdown(self): + self._socket.close() + self._is_alive = False + + def msg(self, message): + + message += '\r\n' + + with self._msg_lock: + + while not self.msg_queue.empty(): + try: + response = self.msg_queue.get_nowait() + except queue.Empty: + break + + try: + self._socket.send(str.encode(message)) + response = self.msg_queue.get() + return response.decode('UTF-8') + + except Exception: + self.shutdown() + raise + + +class SimplePort(SimpleController): + + def __init__(self, host, port): + self._socket = socket(AF_INET, SOCK_STREAM) + self._socket.settimeout(2) + + # This could raise an exception ... + # ... which is intended! + self._socket.connect((host, port)) + + super(SimplePort, self).__init__() + + +class SimpleSocket(SimpleController): + + def __init__(self, socket_path): + from socket import AF_UNIX + self._socket = socket(AF_UNIX, SOCK_STREAM) + self._socket.settimeout(2) + + # This could raise an exception ... + # ... which is intended! + self._socket.connect(socket_path) + + super(SimpleSocket, self).__init__() \ No newline at end of file diff --git a/theonionpack/top/tor.py b/theonionpack/top/tor.py new file mode 100644 index 0000000..2b22b2e --- /dev/null +++ b/theonionpack/top/tor.py @@ -0,0 +1,117 @@ +import collections +import os +import pathlib +import subprocess +import threading +import typing +import uuid + +from shelljob import proc + +from .torhasher import hash_password + +class Tor(): + + def __init__(self, tor: str = '.\Tor', data: str = '.\Data'): + + def find(filename: str, start_at: str = '.') -> typing.Optional[str]: + found = None + p = pathlib.Path(start_at).resolve() + for root, dirs, files in os.walk(str(p)): + if filename in files: + found = os.path.join(root, filename) + break + + return found + + self.process = None + self.owner = None + + self.path = find('tor.exe', tor) + + if self.path is None: + raise FileNotFoundError("Could not find tor.exe.") + + self.geoIP = find('geoip', tor) + self.geoIP6 = find('geoip6', tor) + # self.torrc_defaults = find('torrc-defaults', tor) + self.data = data + + self._messages = collections.deque(maxlen=400) + self.lock = threading.RLock() + + def run(self, owner_pid: int = os.getpid(), password: str = None, additional_command_line: typing.List[str] = None): + + if self.process is not None: + raise OSError('Already running...') + + if self.path is None: + return False + + # pwd = uuid.uuid4().hex + self.password = hash_password(password) if password is not None else None + # print(self.password) + # check = subprocess.run([str(self.path), '--hash-password', pwd], stdout=subprocess.PIPE) + # print(check.stdout.decode('utf-8')) + # print(check.stdout) + + # return + + self.owner = owner_pid + params = [str(self.path)] + + if self.owner > 0: + params.extend(['__OwningControllerProcess', str(owner_pid)]) + + if self.geoIP: + params.extend(['GeoIPFile', self.geoIP]) + if self.geoIP6: + params.extend(['GeoIPv6File', self.geoIP6]) + + params.extend(['+__ControlPort', '9051']) + + if self.password is not None: + params.extend(['__HashedControlSessionPassword', self.password]) + + params.extend(['DataDirectory', self.data]) + + params.extend(['-f', str((pathlib.Path(self.data) / 'torrc/torrc').resolve())]) + params.extend(['--defaults-torrc', str((pathlib.Path(self.data) / 'torrc/torrc-defaults').resolve())]) + params.extend(['--ignore-missing-torrc']) + + # params = [str(self.path)] + + # params.extend(['--hash-password', 'test']) + + # params.extend(['| more']) + + # print(subprocess.list2cmdline(params)) + # self.process = subprocess.Popen(params, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + if additional_command_line is not None: + params.extend(additional_command_line) + + # self.process = subprocess.Popen(params, stderr=subprocess.PIPE) + # print(self.process) + + self.process = proc.Group() + self.process.run(params) + + return self.process + + def collect_messages(self): + if self.process.is_pending(): + self.lock.acquire() + lines = self.process.readlines(timeout=0.25) + for proc, line in lines: + l = line.decode('utf-8').rstrip('\r\n') + if len(l) > 0: + self._messages.append(l) + self.lock.release() + + @property + def messages(self): + self.lock.acquire() + retval = list(self._messages) + self.lock.release() + return retval diff --git a/theonionpack/top/torhasher.py b/theonionpack/top/torhasher.py new file mode 100644 index 0000000..55025c2 --- /dev/null +++ b/theonionpack/top/torhasher.py @@ -0,0 +1,55 @@ +# Based on +# https://gist.github.com/jamesacampbell/2f170fc17a328a638322078f42e04cbc +# 20191225 RDW: adapted to work with Python3 str-type Strings + +import os +import hashlib + + +def hash_password(password: str) -> str: + + # supply password + secret = password.encode('utf-8') + + # static 'count' value later referenced as "c" + indicator = bytes(chr(96), 'ascii') + + # generate salt and append indicator value so that it + salt = os.urandom(8) + salt += indicator + + # That's just the way it is. It's always prefixed with 16 + prefix = '16:' + + # swap variables just so I can make it look exactly like the RFC example + c = salt[8] + + # generate an even number that can be divided in subsequent sections. (Thanks Roman) + EXPBIAS = 6 + count = (16 + (c & 15)) << ((c >> 4) + EXPBIAS) + + d = hashlib.sha1() + + # take the salt and append the password + tmp = salt[:8] + secret + + # hash the salty password as many times as the length of + # the password divides into the count value + slen = len(tmp) + while count: + if count > slen: + d.update(tmp) + count -= slen + else: + d.update(tmp[:count]) + count = 0 + hashed = d.digest() + + # Convert to hex + salt = bytes.hex(salt[:8]).upper() + indicator = bytes.hex(indicator).upper() + torhash = bytes.hex(hashed).upper() + + # Put it all together into the proprietary Tor format. + retval = f'{prefix}{salt}{indicator}{torhash}' + return retval diff --git a/theonionpack/top/util.py b/theonionpack/top/util.py new file mode 100644 index 0000000..3113dfc --- /dev/null +++ b/theonionpack/top/util.py @@ -0,0 +1,41 @@ +import ctypes # An included library with Python install. +import os +import pathlib +import typing +import sys + +## Styles: +## 0 : OK +## 1 : OK | Cancel +## 2 : Abort | Retry | Ignore +## 3 : Yes | No | Cancel +## 4 : Yes | No +## 5 : Retry | No +## 6 : Cancel | Try Again | Continue +# MB_HELP = 0x4000 +# ICON_EXLAIM=0x30 +# ICON_INFO = 0x40 +# ICON_STOP = 0x10 + + +def MBox(text: str, title: str = 'The Onion Pack', style: int = 0): + return ctypes.windll.user32.MessageBoxW(0, text, title, style) + + +def find_file(filename: str, start_at: str = '.') -> typing.Optional[str]: + + def raiser(err): + raise err + + found = None + # print(start_at) + # p = pathlib.WindowsPath(start_at).resolve() + for root, dirs, files in os.walk(start_at, onerror=raiser): + if filename in files: + found = os.path.join(root, filename) + break + + + + + return found