├── .gitignore ├── LICENSE.md ├── README.md ├── RELEASE_NOTES.md ├── Src ├── .paket │ └── Paket.Restore.targets ├── ES.Update.Backend │ ├── ES.Update.Backend.fsproj │ ├── Entities.fs │ ├── UpdateManager.fs │ ├── UpdateService.fs │ ├── Utility.fs │ ├── WebServer.fs │ ├── WebServerLogger.fs │ └── paket.references ├── ES.Update.Releaser │ ├── ES.Update.Releaser.fsproj │ ├── MetadataBuilder.fs │ └── paket.references ├── ES.Update │ ├── CryptoUtility.fs │ ├── ES.Update.fsproj │ ├── Entities.fs │ ├── Installer.fs │ ├── Updater.fs │ ├── Utility.fs │ └── paket.references ├── Examples │ ├── Example1 │ │ ├── Client.cs │ │ ├── Example1.csproj │ │ ├── Helpers.cs │ │ ├── Program.cs │ │ ├── Server.cs │ │ └── paket.references │ ├── Example3 │ │ ├── App.config │ │ ├── AuthenticatedWebServer.cs │ │ ├── Client.cs │ │ ├── Example3.csproj │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── AssemblyInfo.cs │ │ └── paket.references │ └── Example4 │ │ ├── App.config │ │ ├── Client.cs │ │ ├── Example4.csproj │ │ ├── Program.cs │ │ └── Properties │ │ └── AssemblyInfo.cs ├── ProgramUpdaterSln.sln ├── Tests │ └── UnitTests │ │ ├── BackendTests.fs │ │ ├── CryptoUtilityTests.fs │ │ ├── Program.fs │ │ └── UnitTests.fsproj ├── Tools │ ├── KeysGenerator │ │ ├── KeysGenerator.fsproj │ │ ├── Program.fs │ │ └── paket.references │ └── VersionReleaser │ │ ├── Program.fs │ │ ├── Settings.fs │ │ ├── VersionReleaser.fsproj │ │ ├── configuration.json │ │ └── paket.references ├── build.fsx ├── mergeInstaller.fsx ├── nuget.exe ├── paket.dependencies ├── paket.exe └── paket.lock └── build.bat /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | build/ 24 | fake/ 25 | release/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | **/Properties/launchSettings.json 59 | 60 | # StyleCop 61 | StyleCopReport.xml 62 | 63 | # Files built by Visual Studio 64 | *_i.c 65 | *_p.c 66 | *_i.h 67 | *.ilk 68 | *.meta 69 | *.obj 70 | *.iobj 71 | *.pch 72 | *.pdb 73 | *.ipdb 74 | *.pgc 75 | *.pgd 76 | *.rsp 77 | *.sbr 78 | *.tlb 79 | *.tli 80 | *.tlh 81 | *.tmp 82 | *.tmp_proj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | 259 | # Microsoft Fakes 260 | FakesAssemblies/ 261 | 262 | # GhostDoc plugin setting file 263 | *.GhostDoc.xml 264 | 265 | # Node.js Tools for Visual Studio 266 | .ntvs_analysis.dat 267 | node_modules/ 268 | 269 | # Visual Studio 6 build log 270 | *.plg 271 | 272 | # Visual Studio 6 workspace options file 273 | *.opt 274 | 275 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 276 | *.vbw 277 | 278 | # Visual Studio LightSwitch build output 279 | **/*.HTMLClient/GeneratedArtifacts 280 | **/*.DesktopClient/GeneratedArtifacts 281 | **/*.DesktopClient/ModelManifest.xml 282 | **/*.Server/GeneratedArtifacts 283 | **/*.Server/ModelManifest.xml 284 | _Pvt_Extensions 285 | 286 | # Paket dependency manager 287 | .paket/paket.exe 288 | paket-files/ 289 | 290 | # FAKE - F# Make 291 | .fake/ 292 | 293 | # JetBrains Rider 294 | .idea/ 295 | *.sln.iml 296 | 297 | # CodeRush 298 | .cr/ 299 | 300 | # Python Tools for Visual Studio (PTVS) 301 | __pycache__/ 302 | *.pyc 303 | 304 | # Cake - Uncomment if you are using it 305 | # tools/** 306 | # !tools/packages.config 307 | 308 | # Tabs Studio 309 | *.tss 310 | 311 | # Telerik's JustMock configuration file 312 | *.jmconfig 313 | 314 | # BizTalk build output 315 | *.btp.cs 316 | *.btm.cs 317 | *.odx.cs 318 | *.xsd.cs 319 | 320 | # OpenCover UI analysis results 321 | OpenCover/ 322 | 323 | # Azure Stream Analytics local run output 324 | ASALocalRun/ 325 | 326 | # MSBuild Binary and Structured Log 327 | *.binlog 328 | 329 | # NVidia Nsight GPU debugger configuration file 330 | *.nvuser 331 | 332 | # MFractors (Xamarin productivity tool) working folder 333 | .mfractor/ -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## creative commons 2 | 3 | # Attribution-NonCommercial 4.0 International 4 | 5 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 6 | 7 | ### Using Creative Commons Public Licenses 8 | 9 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 10 | 11 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 12 | 13 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 14 | 15 | ## Creative Commons Attribution-NonCommercial 4.0 International Public License 16 | 17 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 18 | 19 | ### Section 1 – Definitions. 20 | 21 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 22 | 23 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 24 | 25 | c. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | d. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | e. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | f. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 32 | 33 | g. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 34 | 35 | h. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 36 | 37 | i. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 38 | 39 | j. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 40 | 41 | k. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 42 | 43 | l. __You__ means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 44 | 45 | ### Section 2 – Scope. 46 | 47 | a. ___License grant.___ 48 | 49 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 50 | 51 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 52 | 53 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 54 | 55 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 56 | 57 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 58 | 59 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 60 | 61 | 5. __Downstream recipients.__ 62 | 63 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 64 | 65 | B. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 66 | 67 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 68 | 69 | b. ___Other rights.___ 70 | 71 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 72 | 73 | 2. Patent and trademark rights are not licensed under this Public License. 74 | 75 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 76 | 77 | ### Section 3 – License Conditions. 78 | 79 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 80 | 81 | a. ___Attribution.___ 82 | 83 | 1. If You Share the Licensed Material (including in modified form), You must: 84 | 85 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 86 | 87 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 88 | 89 | ii. a copyright notice; 90 | 91 | iii. a notice that refers to this Public License; 92 | 93 | iv. a notice that refers to the disclaimer of warranties; 94 | 95 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 96 | 97 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 98 | 99 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 100 | 101 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 102 | 103 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 104 | 105 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 106 | 107 | ### Section 4 – Sui Generis Database Rights. 108 | 109 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 110 | 111 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 112 | 113 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 114 | 115 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 116 | 117 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 118 | 119 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 120 | 121 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 122 | 123 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 124 | 125 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 126 | 127 | ### Section 6 – Term and Termination. 128 | 129 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 130 | 131 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 132 | 133 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 134 | 135 | 2. upon express reinstatement by the Licensor. 136 | 137 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 138 | 139 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 140 | 141 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 142 | 143 | ### Section 7 – Other Terms and Conditions. 144 | 145 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 146 | 147 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 148 | 149 | ### Section 8 – Interpretation. 150 | 151 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 152 | 153 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 154 | 155 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 156 | 157 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 158 | 159 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 160 | > 161 | > Creative Commons may be contacted at creativecommons.org -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Program Updater Framework 2 | A framework to automatize the process of updating a program in an **efficent** and **secure way**. The updates are provided by a server component through and HTTP/HTTPS channel. 3 | 4 | It was created with the following intents: 5 | 6 | * to be very easy to use and to integrate 7 | * to provide an high secure update process even on a not encrypted channel like HTTP (the integrity of all files is checked before being used) 8 | * to be efficient, this means that if your new release just changed one file you don't need to download the full application but only the changed files 9 | * to be autoconsistent, you don't need any other external software (web server, database, ...) to create an update solution 10 | 11 | ## Download 12 | 13 | A pre-compiled file of the framework can be downloaded from the Release section. 14 | 15 | ## Resolving _Could not load file or assembly FSharp.Core_ 16 | 17 | If your project uses a different **FSharp.Core** version you may encounter the following error on startup: 18 | 19 | ```` 20 | System.IO.FileLoadException: 'Could not load file or assembly 'FSharp.Core, Version=4.6.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)' 21 | ```` 22 | 23 | In order to solve it you have to modify your **App.config** configuration file in order to add a redirect. For example, if your are using **FSharp.Core version 4.5.0.0** you may want to add the following content to the **App.config** file: 24 | 25 | ````xml 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ```` 38 | 39 | ## Core Concepts 40 | 41 | The framework can be used via the command line tools or by integrating it in your web application. In both cases the process to release a new update is composed of the following three steps: 42 | 43 | * Create the metadata related to the new update 44 | * Push the metadata to the update server (this step can be merged with the one above) 45 | * Run the update program from the client 46 | 47 | In order to setup an update process you need: 48 | 49 | * A folder where all the metadata are saved 50 | * To follow a naming convention for your update file (only zip file are supported for now). The convention is that the name must be something like: MyApplication.1.2.3.zip. 51 | * To generate and distribute the encryption key (this process is done automatically on first server execution) 52 | 53 | ## Configuration File 54 | 55 | All the command line options can also be specified in the given configuration file for each tools. The deafult name for the configuration file is **configuration.json** and it is in JSON format. If a command line value is specified it will take precedence over the value set in the configuration file. 56 | 57 | # Examples 58 | 59 | Below you can find some examples that should provide enough information to use the framework proficiently. 60 | 61 | ## Example 1 62 | 63 | The goal of this example is to provide a full update process by only using the commant line utilities. We will suppose that we have four versions of our software and we want to release a new version 5.0. We will use the _update_ directory in order to store the information related to the updates of our software. 64 | 65 |
Details 66 |

67 |

Step 0 - Start up

68 | 69 | If you have never used the framework to provide updates to your clients, it is a good practice to follow the _Step 1_ for each release of your software, starting from the oldest to the newest. 70 | 71 |

Step 1 - Metadata Creation

72 | 73 | The first step is to create the metadata, this is done with the **VersionReleaser.exe** tool. We run the following command: 74 | 75 | ````bash 76 | VersionReleaser.exe --working-dir updates Examples\Example1\MyApplication.v5.0.zip 77 | -=[ Version Releaser ]=- 78 | Copyright (c) 2019 Enkomio 79 | 80 | [INFO] 2019-08-09 19:26:45 - Analyze release file: MyApplication.v5.0.zip 81 | [INFO] 2019-08-09 19:26:45 - Saving release metadata 82 | [INFO] 2019-08-09 19:26:45 - Saving artifacts to update 83 | [INFO] 2019-08-09 19:26:45 - Adding new file 'folder\file8.txt' as 77C6EC70B75CE3254B910DC6073DB04A61E2EB5273191F73B0AB539F6CAD43C2 84 | [INFO] 2019-08-09 19:26:45 - Process completed 85 | ```` 86 | Now the metadata are created and the new artifacts are saved. You can exclude some files from the update process, this is very important for configuration file or local database. You can configure the patterns of the files to exclude in the **configuration.json** file. The current list can be found here. 87 | 88 |

Step 2 - Start the update server

89 | 90 | Now you have to start the update server. The framework provides a program named **UpdateServer.exe** that will run a web server in order to accept update requests. You can do this with the following command: 91 | ````bash 92 | UpdateServer.exe --working-dir updates 93 | -=[ Version Releaser ]=- 94 | Copyright (c) 2019 Enkomio 95 | 96 | [INFO] 2019-08-10 15:06:48 - Encryption keys not found. Generating them 97 | [INFO] 2019-08-10 15:06:48 - Encryption keys created and saved to files. The public key must be distributed togheter with the updater 98 | [INFO] 2019-08-10 15:06:48 - Public key: RUNTNUIAAAABQa5NN74/BqJW7Ial8xj2D/QB32Dj7ZuMOmtfIfo4PiHuXD3QiM6xvOvEZbJ1vQPdjUignHYE7BCLdslEMYbCj4AA8QeSc9v7jc1X5cqKCL1tHaJc+B/MWp8sRXlL6wYUJj4bfcC3p/xEJZXeO/RUsO8gKA4KT0UAXsq0bExWRQr6Ioc= 99 | [INFO] 2019-08-10 15:06:48 - Loaded project MyApplication version 1.0 100 | [INFO] 2019-08-10 15:06:48 - Loaded project MyApplication version 2.0 101 | [INFO] 2019-08-10 15:06:48 - Loaded project MyApplication version 3.0 102 | [INFO] 2019-08-10 15:06:48 - Loaded project MyApplication version 4.0 103 | [INFO] 2019-08-10 15:06:48 - Loaded project MyApplication version 5.0 104 | [17:06:48 INF] Smooth! Suave listener started in 86.698ms with binding 127.0.0.1:80 105 | ```` 106 | The server recognizes that we defined five applications. It is also very important to take note of the *public key*. This value must be set in the client in order to ensure the integrity of the updates. 107 | 108 |

Step 3 - Run the update client

109 | 110 | The final step of this example is to update the client code by connecting to the server. In order to do this, it is necessary to specify the following information: 111 | 112 | * The address of the update server 113 | * The public key of the server 114 | * The name of the project that must be updated 115 | 116 | The first two information can be retrieved from the output of the server in the previous step. We suppose that the update must be installed in the current directory (a very common case if you distribute the update program togheter with your binary), if this is not the case you can change this value with the _--directory_ argument. You can now run the following command: 117 | ````bash 118 | Updater.exe --project MyApplication --server-uri http://127.0.0.1 --server-key "RUNTNUIAAAABQa5NN74/BqJW7Ial8xj2D/QB32Dj7ZuMOmtfIfo4PiHuXD3QiM6xvOvEZbJ1vQPdjUignHYE7BCLdslEMYbCj4AA8QeSc9v7jc1X5cqKCL1tHaJc+B/MWp8sRXlL6wYUJj4bfcC3p/xEJZXeO/RUsO8gKA4KT0UAXsq0bExWRQr6Ioc=" 119 | -=[ Program Updater ]=- 120 | Copyright (c) 2019 Enkomio 121 | 122 | [INFO] 2019-08-13 14:31:28 - Found a more recent version: 5.0. Start update 123 | [INFO] 2019-08-13 14:31:28 - Project 'MyApplication' was updated to version '5.0' in directory: . 124 | ```` 125 | If you now take a look at the current directory you will see that new files were created due to the update process. 126 |

127 |
128 | 129 | ## Example 2 130 | 131 | The goal of this example is to show how to use the library in order to create a custom update. The result will be the same as the previous example. You can find the related files in the Example 2 folder. 132 | 133 |
Details 134 |

135 | 136 | ### Step 1 - Metadata Creation 137 | 138 | The most common case when you have to generate the metada for a new release is to use the command line utility. If for some reason you want to use the library you must use the **MetadataBuilder** class and specify the working directory where the metadata will be saved. 139 | 140 | An example of usage is: 141 | ````csharp 142 | var metadataBuilder = new MetadataBuilder(workspaceDirectory); 143 | metadataBuilder.CreateReleaseMetadata(fileName); 144 | ```` 145 | ### Step 2 - Start the update server 146 | 147 | The framework provides a **WebServer** class that can be used to run the update server. The web server is based on the Suave project. To run a web server you have to specify: 148 | 149 | * The binding base URI 150 | * The workspace directory where the metadata are stored 151 | * The private key 152 | 153 | To generate a new pair of public and private keys you can use the **CryptoUtility.GenerateKeys** method. Find below an example of code that starts a web server. 154 | ````csharp 155 | var (publicKey, privateKey) = CryptoUtility.GenerateKeys(); 156 | var server = new WebServer(this.BindingUri, this.WorkspaceDirectory, privateKey); 157 | ```` 158 | ### Step 3 - Implement the update client 159 | 160 | The last step is to integrate the update client in your solution. In this case you need the following information: 161 | 162 | * The server base URI 163 | * The server public key 164 | * The name of the project that you want to update 165 | * The current project version 166 | * The destination directory where the update must be installed 167 | 168 | All information should alredy know if you followed the Step 2. Now you can update your client with the following code: 169 | ````csharp 170 | var applicationVersion = new Version(3, 0); 171 | var updater = new Updater(serverBaseUri, applicationName, applicationVersion, destinationDirectory, serverPublicKey); 172 | 173 | var latestVersion = updater.GetLatestVersion(); 174 | if (latestVersion > applicationVersion) 175 | { 176 | var updateResult = updater.Update(applicationVersion); 177 | if (updateResult.Success) 178 | { 179 | // Update ok 180 | } 181 | else 182 | { 183 | // Error 184 | } 185 | } 186 | ```` 187 |

188 |
189 | 190 | ## Example 3 191 | 192 | The goal of this example is to show how to customize the web server. Often the update must be provided only to clients that have the needed authorization, in this example we will see how to authenticate the update requests. You can find the example files in the Example 3 folder. 193 | 194 |
Details 195 |

196 | 197 | ### Step 0 - Installing dependency 198 | 199 | The framework uses Suave in order to implements the web server. In case of simple use of the ProgramUpdater framework, you don't have to worry about it but in this example it is necessary to reference it in order to use its classes. You can use Paket to reference it or add it via NuGet. 200 | 201 | ### Step 1 - Metadata Creation 202 | 203 | See *Example 1* Step 1 204 | 205 | ### Step 2 - Start the update server 206 | 207 | For this example we will create a sub-class of the **WebServer** framework class and we override the **Authenticate** method in order to verify the credentials that will be sent by the updater. 208 | 209 | Below you can find the relevant code that checks if the credentials are correct: 210 | ````csharp 211 | var formParameters = Encoding.UTF8.GetString(ctx.request.rawForm).Split('&'); 212 | var username = String.Empty; 213 | var password = String.Empty; 214 | 215 | foreach(var parameter in formParameters) 216 | { 217 | var nameValue = parameter.Split('='); 218 | if (nameValue[0].Equals("Username", StringComparison.OrdinalIgnoreCase)) 219 | { 220 | username = nameValue[1]; 221 | } 222 | else if (nameValue[0].Equals("Password", StringComparison.OrdinalIgnoreCase)) 223 | { 224 | password = nameValue[1]; 225 | } 226 | } 227 | 228 | return 229 | username.Equals(AuthenticatedWebServer.Username, StringComparison.Ordinal) 230 | && password.Equals(AuthenticatedWebServer.Password, StringComparison.Ordinal); 231 | ```` 232 | Here you can find the full source code of the **AuthenticatedWebServer** class. 233 | 234 | ### Step 3 - Implement the update client 235 | 236 | In this case the difference with the previous example is that we have to authenticate to the server. This is an easy step if we know the username and password. We just have to add these info to the update request. This is easily done with the following code: 237 | ````csharp 238 | // add username and password to the update request 239 | updater.AddParameter("username", AuthenticatedWebServer.Username); 240 | updater.AddParameter("password", AuthenticatedWebServer.Password); 241 | ```` 242 | The specified parameters will be added to the update request and will be used to verify the authentication. At his time it is possible to specify only POST parameters. 243 | 244 | Here you can find the full source code of the **Client** class. 245 | 246 |

247 |
248 | 249 | ## Example 4 250 | 251 | The goal of this example is to provide a flexible update method by invoking an external program to do the update. Often the update method is not just a matter of copy the files to the destination directory but other, more complex, tasks must be done. The full source code of this example can be find in the Example 4 folder. 252 | 253 |
Details 254 |

255 | 256 | In version 1.1 was released a new feature that allows to invoke an external program in order to do the installation. The framework provides an **Installer** program that copy the files to a destination directory. Using this approach is the suggested one, since it will avoid to have update problems when you have to update the current running program (you cannot write on a file associated to a running process). In order achieve this, when an external Installer is used, the update process is terminated in order to avoid conflict. If you want to be sure that the parent exited just wait on the mutex name composed from the arguments hashes, to know more take a look at the mutex name generation code. 257 | 258 | Of course you can use your own installer program, you have just to add it to the configuration (we will see how to do it). The only rules that must be respected are: 259 | 260 | * The name of the installer program must be **Installer.exe** 261 | * It accepts the following arguments: 262 | * **--source** that is the directory where the new files are stored 263 | * **--dest** that is the directory that must be updated with the new files 264 | 265 | ### Step 1 - Metadata Creation 266 | 267 | See *Example 1* Step 1. 268 | 269 | ### Step 2 - Start the update server 270 | 271 | This step is very similar to *Example 2* Step 2. The main difference is that you have to specify the directory where your installer is stored. All the files in this directory will be copied in the update package sent to the client. In the listing below you can see an example that use the **Installer.exe** provided by the framework: 272 | 273 | ````csharp 274 | // set the installer path 275 | var installerPath = Path.GetDirectoryName(typeof(Installer.Program).Assembly.Location); 276 | _server.WebServer.InstallerPath = installerPath; 277 | ```` 278 | The **Installer** program from the framework by default will start again the main process with the specified arguments. 279 | 280 | For security reason the framework will add the integrity info about the installer inside the update package. These info will be checked by the client before to invoke the installer. 281 | 282 | ### Step 3 - Run the update client 283 | 284 | See Step 3 of previous examples. 285 | 286 |

287 |
288 | 289 | # Security 290 | 291 | The update process use **ECDSA** with **SHA-256** in order to ensure the integrity of the update. The *public* and *private* keys are automatically generated on first start and saved to local files (*public.txt* and *private.txt*). 292 | 293 | ## Exporting the private key 294 | 295 | In order to protect the private key from an attacker that is able to read arbitrary files from your filesystem, the key is AES encrypted with parameters that are related to the execution environment (MAC address, properties of the installed HDs). This means that you cannot just copy the private key file from one computer to another, since it will not work. If you want to obtain an exportable private key you have to export it by executing the following command: 296 | 297 | UpdateServer.exe --export-key clean-private-key.txt 298 | -=[ Version Releaser ]=- 299 | Copyright (c) 2019 Enkomio 300 | 301 | Enter password: **** 302 | Re-enter password: **** 303 | [INFO] 2019-08-27 17:02:33 - Private key exported to file: clean-private-key.txt 304 | 305 | The exported key is AES encrypted with the input password. 306 | 307 | ## Importing private key 308 | 309 | If you want to import a private key that was exported from another server you can do it by run the following command: 310 | 311 | UpdateServer.exe --import-key clean-private-key.txt 312 | -=[ Version Releaser ]=- 313 | Copyright (c) 2019 Enkomio 314 | 315 | Enter password: **** 316 | Re-enter password: **** 317 | [INFO] 2019-08-27 17:04:14 - Private key from file 'clean-private-key.txt' imported. Be sure to set the public key accordingly. 318 | 319 | This command will read and save the private key in an encrypted form with the new parameters of the new server. You have also to copy the public key to the server. 320 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | ### 1.3 - 20/09/2019 2 | * Added --skip-on-exist option to Updater.exe to specify patterns for files that must be copied only if not exist 3 | * Added SkipIntegrityCheck property to Updater class to ignore integrity check (useful during development) 4 | * Removed args and exec option from installer since considered not very useful 5 | * Minor improvements 6 | 7 | ### 1.2.0 - 29/08/2019 8 | * Added utility to generate public and private key from command line 9 | * Added timer to avoid to restart program when a new update is available 10 | * Moved to Bouncy Castle for encryption in order to be supported on Mono too 11 | * Fixed minor error 12 | 13 | ### 1.1.0 - 28/08/2019 14 | * Improved installer by invoking an external program to avoid conflict on copy. 15 | * Exporting and importing the private key now require a password to save the value in an encrypted form. This will ensure that the private key is never stored in clear text. 16 | * Minor bug fixing and code improvement. 17 | 18 | ### 1.0.0 - 19/08/2019 19 | * First Release. 20 | -------------------------------------------------------------------------------- /Src/.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | $(MSBuildVersion) 10 | 15.0.0 11 | false 12 | true 13 | 14 | true 15 | $(MSBuildThisFileDirectory) 16 | $(MSBuildThisFileDirectory)..\ 17 | $(PaketRootPath)paket-files\paket.restore.cached 18 | $(PaketRootPath)paket.lock 19 | classic 20 | proj 21 | assembly 22 | native 23 | /Library/Frameworks/Mono.framework/Commands/mono 24 | mono 25 | 26 | 27 | $(PaketRootPath)paket.bootstrapper.exe 28 | $(PaketToolsPath)paket.bootstrapper.exe 29 | $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ 30 | 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | True 42 | 43 | 44 | False 45 | 46 | $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | $(PaketRootPath)paket 56 | $(PaketToolsPath)paket 57 | 58 | 59 | 60 | 61 | 62 | $(PaketRootPath)paket.exe 63 | $(PaketToolsPath)paket.exe 64 | 65 | 66 | 67 | 68 | 69 | <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) 70 | <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) 71 | <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | <_PaketCommand>dotnet paket 83 | 84 | 85 | 86 | 87 | 88 | $(PaketToolsPath)paket 89 | $(PaketBootStrapperExeDir)paket 90 | 91 | 92 | paket 93 | 94 | 95 | 96 | 97 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 98 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" 99 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 100 | <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | true 122 | $(NoWarn);NU1603;NU1604;NU1605;NU1608 123 | false 124 | true 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 134 | 135 | 136 | 137 | 138 | 139 | 141 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) 142 | $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) 143 | 144 | 145 | 146 | 147 | %(PaketRestoreCachedKeyValue.Value) 148 | %(PaketRestoreCachedKeyValue.Value) 149 | 150 | 151 | 152 | 153 | true 154 | false 155 | true 156 | 157 | 158 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached 183 | 184 | $(MSBuildProjectFullPath).paket.references 185 | 186 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 187 | 188 | $(MSBuildProjectDirectory)\paket.references 189 | 190 | false 191 | true 192 | true 193 | references-file-or-cache-not-found 194 | 195 | 196 | 197 | 198 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 199 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 200 | references-file 201 | false 202 | 203 | 204 | 205 | 206 | false 207 | 208 | 209 | 210 | 211 | true 212 | target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | false 224 | true 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) 236 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 237 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 238 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 239 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) 240 | 241 | 242 | %(PaketReferencesFileLinesInfo.PackageVersion) 243 | All 244 | runtime 245 | runtime 246 | true 247 | true 248 | 249 | 250 | 251 | 252 | $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 262 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 263 | 264 | 265 | %(PaketCliToolFileLinesInfo.PackageVersion) 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 | 278 | false 279 | 280 | 281 | 282 | 283 | 284 | <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> 285 | 286 | 287 | 288 | 289 | 290 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 291 | true 292 | false 293 | true 294 | false 295 | true 296 | false 297 | true 298 | false 299 | true 300 | $(PaketIntermediateOutputPath)\$(Configuration) 301 | $(PaketIntermediateOutputPath) 302 | 303 | 304 | 305 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 363 | 364 | 407 | 408 | 450 | 451 | 492 | 493 | 494 | 495 | -------------------------------------------------------------------------------- /Src/ES.Update.Backend/ES.Update.Backend.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Src/ES.Update.Backend/Entities.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Backend 2 | 3 | open System 4 | 5 | module Entities = 6 | let DefaultVersion = "0.0" 7 | 8 | type File = { 9 | Path: String 10 | ContentHash: String 11 | } 12 | 13 | type Application = { 14 | Version: Version 15 | Files: File array 16 | } 17 | -------------------------------------------------------------------------------- /Src/ES.Update.Backend/UpdateManager.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Backend 2 | 3 | open System 4 | open System.Timers 5 | open System.IO 6 | open System.Text 7 | open ES.Update.Backend.Entities 8 | 9 | type UpdateManager(workingDirectory: String) = 10 | let _lock = new Object() 11 | let _timer = new Timer() 12 | let mutable _applications : Application array = Array.empty 13 | 14 | let populateKnowledgeBase() = 15 | _timer.Stop() 16 | lock _lock (fun () -> 17 | let versionDir = Path.Combine(workingDirectory, "Versions") 18 | if Directory.Exists(versionDir) then 19 | _applications <- 20 | Directory.GetFiles(versionDir) 21 | |> Array.map(fun fileName -> 22 | { 23 | Version = Version.Parse(Path.GetFileNameWithoutExtension(fileName)) 24 | Files = 25 | File.ReadAllLines(fileName) 26 | |> Array.map(fun line -> line.Trim().Split(',')) 27 | |> Array.map(fun items -> (items.[0], String.Join(",", items.[1..]))) 28 | |> Array.map(fun (hashValue, path) -> {ContentHash = hashValue; Path = path}) 29 | } 30 | ) 31 | ) 32 | _timer.Start() 33 | 34 | do 35 | if Directory.Exists(workingDirectory) then 36 | populateKnowledgeBase() 37 | 38 | // add directory watcher 39 | _timer.Interval <- TimeSpan.FromMinutes(1.).TotalMilliseconds |> float 40 | _timer.Elapsed.Add(fun _ -> populateKnowledgeBase()) 41 | 42 | abstract GetAvailableVersions: unit -> Version array 43 | default this.GetAvailableVersions() = 44 | _applications 45 | |> Seq.toArray 46 | |> Array.map(fun application -> application.Version) 47 | 48 | abstract GetApplication: Version -> Application option 49 | default this.GetApplication(version: Version) = 50 | _applications |> Seq.tryFind(fun app -> app.Version = version) 51 | 52 | abstract GetLatestVersion: unit -> Application option 53 | default this.GetLatestVersion() = 54 | _applications 55 | |> Seq.sortByDescending(fun application -> application.Version) 56 | |> Seq.tryHead 57 | 58 | abstract ComputeCatalog: File array -> String 59 | default this.ComputeCatalog(files: File array) = 60 | let fileContent = new StringBuilder() 61 | files 62 | |> Array.iter(fun file -> 63 | fileContent.AppendFormat("{0},{1}\r\n", file.ContentHash, file.Path) |> ignore 64 | ) 65 | fileContent.ToString() -------------------------------------------------------------------------------- /Src/ES.Update.Backend/UpdateService.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Backend 2 | 3 | open System 4 | open System.Timers 5 | open System.IO 6 | open System.Collections.Concurrent 7 | open ES.Update 8 | open System.Text 9 | 10 | type UpdateService(workspaceDirectory: String, privateKey: Byte array) = 11 | let _lock = new Object() 12 | let _timer = new Timer() 13 | let _updateManagers = new ConcurrentDictionary() 14 | let _syncRoot = new Object() 15 | let mutable _allFiles = Array.empty 16 | 17 | let getUpdateManager(projectName: String) = 18 | _updateManagers.[projectName] 19 | 20 | let doUpdate() = 21 | _timer.Stop() 22 | lock _lock (fun _ -> 23 | // read all version 24 | Directory.GetDirectories(workspaceDirectory) 25 | |> Array.map(fun directory -> Path.GetFileName(directory)) 26 | |> Array.iter(fun projectName -> 27 | let projectDirectory = Path.Combine(workspaceDirectory, projectName) 28 | Directory.CreateDirectory(projectDirectory) |> ignore 29 | _updateManagers.[projectName] <- new UpdateManager(projectDirectory) 30 | ) 31 | 32 | // read all available files 33 | let fileBucketDirectory = Path.Combine(workspaceDirectory, "FileBucket") 34 | let allFiles = Directory.GetFiles(fileBucketDirectory, "*", SearchOption.AllDirectories) 35 | lock _syncRoot (fun () -> _allFiles <- allFiles) 36 | ) 37 | _timer.Start() 38 | 39 | do 40 | _timer.Interval <- TimeSpan.FromMinutes(1.).TotalMilliseconds |> float 41 | _timer.Elapsed.Add(fun _ -> doUpdate()) 42 | doUpdate() 43 | 44 | member this.GetLatestVersion(projectName: String) = 45 | lock _lock (fun _ -> 46 | if _updateManagers.ContainsKey(projectName) then 47 | match getUpdateManager(projectName).GetLatestVersion() with 48 | | Some application -> Some <| application.Version.ToString() 49 | | None -> None 50 | else 51 | None 52 | ) 53 | 54 | member this.IsValidProject(projectName: String) = 55 | _updateManagers.ContainsKey(projectName) 56 | 57 | member this.GetCatalog(version: Version, projectName: String) = 58 | let updateManager = getUpdateManager(projectName) 59 | match updateManager.GetLatestVersion() with 60 | | Some application when application.Version > version -> 61 | let catalog = updateManager.ComputeCatalog(application.Files) 62 | let signature = CryptoUtility.hexSign(Encoding.UTF8.GetBytes(catalog), privateKey) 63 | let result = String.Format("{0}\r\n{1}", signature, catalog) 64 | result 65 | | _ -> String.Empty 66 | 67 | member this.GetAvailableVersions() = 68 | lock _lock (fun _ -> 69 | _updateManagers 70 | |> Seq.toArray 71 | |> Array.collect(fun kv -> 72 | kv.Value.GetAvailableVersions() 73 | |> Array.map(fun version -> (kv.Key, version)) 74 | ) 75 | ) 76 | 77 | member this.GetFilePath(hash: String) = 78 | lock _syncRoot (fun () -> 79 | _allFiles 80 | |> Array.tryFind(fun file -> file.EndsWith(hash, StringComparison.OrdinalIgnoreCase)) 81 | ) -------------------------------------------------------------------------------- /Src/ES.Update.Backend/Utility.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Backend 2 | 3 | open System 4 | open System.IO 5 | open System.IO.Compression 6 | open System.Text 7 | open Suave 8 | open Entities 9 | open System.Security.Cryptography 10 | open ES.Update 11 | 12 | [] 13 | module Utility = 14 | let private readIntegrityInfo(zipFile: String) = 15 | use zipStream = File.OpenRead(zipFile) 16 | use zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read) 17 | let catalogEntry = 18 | zipArchive.Entries 19 | |> Seq.find(fun entry -> entry.FullName.Equals("catalog", StringComparison.OrdinalIgnoreCase)) 20 | 21 | use zipStream = catalogEntry.Open() 22 | use memStream = new MemoryStream() 23 | zipStream.CopyTo(memStream) 24 | Encoding.UTF8.GetString(memStream.ToArray()) 25 | 26 | let private addEntry(zipFile: String, name: String, content: Byte array) = 27 | use zipStream = File.Open(zipFile, FileMode.Open) 28 | use zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Update) 29 | let zipEntry = zipArchive.CreateEntry(name) 30 | use zipEntryStream = zipEntry.Open() 31 | zipEntryStream.Write(content, 0, content.Length) 32 | 33 | let addSignature(zipFile: String, privateKey: Byte array) = 34 | // compute signature and add it to the new file 35 | let integrityInfo = readIntegrityInfo(zipFile) 36 | let signature = CryptoUtility.sign(Encoding.UTF8.GetBytes(integrityInfo), privateKey) 37 | addEntry(zipFile, "signature", signature) 38 | 39 | let private getRelativePath(fileName: String, basePath: String) = 40 | fileName.Replace(basePath, String.Empty).TrimStart(Path.DirectorySeparatorChar) 41 | 42 | let addInstaller(zipFile: String, installerPath: String, privateKey: Byte array) = 43 | // compute the integrtiy info for the installer 44 | let integrityInfo = new StringBuilder() 45 | Directory.GetFiles(installerPath, "*.*", SearchOption.AllDirectories) 46 | |> Array.iter(fun fileName -> 47 | let relativePath = getRelativePath(fileName, installerPath) 48 | let hashValue = sha256(File.ReadAllBytes(fileName)) 49 | integrityInfo.AppendFormat("{0},{1}", hashValue, relativePath).AppendLine() |> ignore 50 | ) 51 | 52 | // sign the integrity info and add the catalog 53 | let catalog = Encoding.UTF8.GetBytes(integrityInfo.ToString()) 54 | let signature = CryptoUtility.sign(catalog, privateKey) 55 | addEntry(zipFile, "installer-signature", signature) 56 | addEntry(zipFile, "installer-catalog", catalog) 57 | 58 | // add all files from installerPath to the zip file 59 | Directory.GetFiles(installerPath, "*.*", SearchOption.AllDirectories) 60 | |> Array.iter(fun fileName -> 61 | let relativePath = getRelativePath(fileName, installerPath) 62 | addEntry(zipFile, relativePath, File.ReadAllBytes(fileName)) 63 | ) 64 | 65 | let createZipFile(zipFile: String, files: (File * Byte array) list, integrityInfo: String) = 66 | use zipStream = File.OpenWrite(zipFile) 67 | use zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create) 68 | 69 | // add integrity info and files 70 | let files = files |> List.map(fun (f, c) -> (f.ContentHash, c)) 71 | ("catalog", integrityInfo |> Encoding.UTF8.GetBytes)::files 72 | |> List.iter(fun (name, content) -> 73 | let zipEntry = zipArchive.CreateEntry(name) 74 | use zipEntryStream = zipEntry.Open() 75 | zipEntryStream.Write(content, 0, content.Length) 76 | ) 77 | 78 | let private getPostParameters(ctx: HttpContext) = 79 | // this method is necessary since Suave seems to have some problems in parsing params 80 | let data = Encoding.UTF8.GetString(ctx.request.rawForm) 81 | data.Split([|'&'|]) 82 | |> Array.map(fun v -> v.Split([|'='|])) 83 | |> Array.filter(fun items -> items.Length > 1) 84 | |> Array.map(fun items -> (items.[0], String.Join("=", items.[1..]))) 85 | 86 | let tryGetPostParameters(parameters: String list, ctx: HttpContext) = 87 | getPostParameters(ctx) 88 | |> Array.filter(fun (name, _) -> parameters |> List.contains name) 89 | |> fun resultValues -> 90 | if resultValues.Length = parameters.Length 91 | then Some(dict resultValues) 92 | else None -------------------------------------------------------------------------------- /Src/ES.Update.Backend/WebServer.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Backend 2 | 3 | open System 4 | open System.Net 5 | open System.Threading 6 | open Suave 7 | open Suave.Successful 8 | open Suave.Writers 9 | open Suave.Operators 10 | open Suave.RequestErrors 11 | open Suave.Filters 12 | open Suave.Files 13 | open ES.Fslog 14 | 15 | type WebServer(binding: Uri, workspaceDirectory: String, privateKey: Byte array, logProvider: ILogProvider) as this = 16 | let _shutdownToken = new CancellationTokenSource() 17 | let _updateService = new UpdateService(workspaceDirectory, privateKey) 18 | let _logger = 19 | let tmp = new WebServerLogger() 20 | logProvider.AddLogSourceToLoggers(tmp) 21 | tmp 22 | 23 | let getFileFullPath(fileName: String) (ctx: HttpContext) = async { 24 | match _updateService.GetFilePath(fileName) with 25 | | Some filePath -> 26 | let newState = ctx.userState.Add("file", filePath).Add("name", fileName) 27 | return Some {ctx with userState = newState} 28 | | None -> 29 | _logger.FileNotFound(fileName) 30 | return None 31 | } 32 | 33 | let preFilter (oldCtx: HttpContext) = async { 34 | let! ctx = addHeader "X-Xss-Protection" "1; mode=block" oldCtx 35 | let! ctx = addHeader "Content-Security-Policy" "script-src 'self' 'unsafe-inline' 'unsafe-eval'" ctx.Value 36 | let! ctx = addHeader "X-Frame-Options" "SAMEORIGIN" ctx.Value 37 | let! ctx = addHeader "X-Content-Type-Options" "nosniff" ctx.Value 38 | return Some ctx.Value 39 | } 40 | 41 | let postFilter (ctx: HttpContext) = async { 42 | _logger.LogRequest(ctx) 43 | return (Some ctx) 44 | } 45 | 46 | let index(ctx: HttpContext) = 47 | OK this.IndexPage ctx 48 | 49 | let latest(ctx: HttpContext) = 50 | match ctx.request.queryParam "project" with 51 | | Choice1Of2 projectName -> 52 | match _updateService.GetLatestVersion(projectName) with 53 | | Some version -> OK version ctx 54 | | None -> OK Entities.DefaultVersion ctx 55 | | _ -> 56 | OK Entities.DefaultVersion ctx 57 | 58 | let updates(ctx: HttpContext) = 59 | let inputVersion = ref(new Version()) 60 | match tryGetPostParameters(["version"; "project"], ctx) with 61 | | Some values 62 | when 63 | Version.TryParse(values.["version"], inputVersion) 64 | && _updateService.IsValidProject(values.["project"]) 65 | -> 66 | 67 | let projectName = values.["project"] 68 | let catalog = _updateService.GetCatalog(!inputVersion, projectName) 69 | OK catalog ctx 70 | | _ -> 71 | BAD_REQUEST "Missing parameters" ctx 72 | 73 | let downloadFile(ctx: HttpContext) = 74 | let file = ctx.userState.["file"] :?> String 75 | let name = ctx.userState.["name"] :?> String 76 | addHeader "Content-Type" "application/octet-stream" 77 | >=> addHeader "Content-Disposition" ("inline; filename=\"" + name + "\"") 78 | >=> sendFile file true 79 | <| ctx 80 | 81 | let authorize (webPart: WebPart) (ctx: HttpContext) = 82 | if this.Authenticate(ctx) 83 | then webPart ctx 84 | else FORBIDDEN "Forbidden" ctx 85 | 86 | let pathNotFound(p: String)(ctx: HttpContext) = 87 | NOT_FOUND "Path not valid" ctx 88 | 89 | let buildCfg(uri: Uri) = 90 | { defaultConfig with 91 | bindings = [HttpBinding.create HTTP (IPAddress.Parse uri.Host) (uint16 uri.Port)] 92 | listenTimeout = TimeSpan.FromMilliseconds (float 10000) 93 | cancellationToken = _shutdownToken.Token 94 | } 95 | 96 | do 97 | _logger.SettingInfo("Workspace Directory", workspaceDirectory) 98 | _logger.SettingInfo("Binding Uri", binding.ToString()) 99 | 100 | new (binding: Uri, workspaceDirectory: String, privateKey: Byte array) = new WebServer(binding, workspaceDirectory, privateKey, LogProvider.GetDefault()) 101 | 102 | member val IndexPage = "-=[ Enkomio Updater Server ]=-" with get, set 103 | 104 | /// This parameter can specify a uri path prefix to use when invoking endpoints 105 | member val PathPrefix = String.Empty with get, set 106 | 107 | /// The path where the installer program is stored. If this path exists an Installer will be pushed in the update package 108 | member val InstallerPath = String.Empty with get, set 109 | 110 | abstract GetRoutes: unit -> WebPart list 111 | default this.GetRoutes() = [ 112 | GET >=> preFilter >=> choose [ 113 | path (this.PathPrefix + "/") >=> index 114 | path (this.PathPrefix + "/latest") >=> latest 115 | pathScan "/%s" pathNotFound 116 | ] >=> postFilter 117 | 118 | POST >=> preFilter >=> choose [ 119 | // get the catalog for the specified version 120 | path (this.PathPrefix + "/updates") >=> authorize updates 121 | 122 | // get the specified file 123 | pathScan (PrintfFormat<_, _, _, _, String>(this.PathPrefix + "/file/%s")) getFileFullPath >=> authorize downloadFile 124 | pathScan "/%s" pathNotFound 125 | ] >=> postFilter 126 | ] 127 | 128 | abstract Authenticate: HttpContext -> Boolean 129 | default this.Authenticate(ctx: HttpContext) = 130 | true 131 | 132 | member this.Start() = 133 | logProvider.AddLogSourceToLoggers(_logger) 134 | 135 | _updateService.GetAvailableVersions() 136 | |> Array.iter(fun (prj, ver) -> _logger.VersionInfo(prj, ver)) 137 | 138 | // start web server 139 | let cfg = buildCfg(binding) 140 | let routes = this.GetRoutes() |> choose 141 | startWebServer cfg routes 142 | 143 | member this.Stop() = 144 | _shutdownToken.Cancel() 145 | 146 | 147 | -------------------------------------------------------------------------------- /Src/ES.Update.Backend/WebServerLogger.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Backend 2 | 3 | open System 4 | open System.Text 5 | open ES.Fslog 6 | open Suave 7 | 8 | type WebServerLogger() = 9 | inherit LogSource("WebServer") 10 | 11 | [] 12 | member this.LogRequest(ctx: HttpContext, ?logData: Boolean) = 13 | let ip = ctx.clientIpTrustProxy.ToString() 14 | let httpMethod = ctx.request.``method`` 15 | let path = ctx.request.url.PathAndQuery 16 | 17 | let data = 18 | let tmpData = " - Data: " + Encoding.Default.GetString(ctx.request.rawForm) 19 | match (logData, httpMethod) with 20 | | (None, HttpMethod.POST) -> tmpData 21 | | (Some printData, HttpMethod.POST) when printData -> tmpData 22 | | _ -> String.Empty 23 | base.WriteLog(1, ip, httpMethod, path, data, ctx.response.status.code) 24 | 25 | [] 26 | member this.VersionInfo(projectName: String, version: Version) = 27 | base.WriteLog(2, projectName, version) 28 | 29 | [] 30 | member this.FileNotFound(file: String) = 31 | base.WriteLog(3, file) 32 | 33 | [] 34 | member this.SettingInfo(name: String, value: String) = 35 | base.WriteLog(4, name, value) -------------------------------------------------------------------------------- /Src/ES.Update.Backend/paket.references: -------------------------------------------------------------------------------- 1 | Suave 2 | Newtonsoft.Json 3 | ES.Fslog.Core -------------------------------------------------------------------------------- /Src/ES.Update.Releaser/ES.Update.Releaser.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Src/ES.Update.Releaser/MetadataBuilder.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update.Releaser 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.IO 6 | open System.Text 7 | open System.Text.RegularExpressions 8 | open System.IO.Compression 9 | open ES.Fslog 10 | open ES.Update 11 | 12 | type MetadataBuilder(workingDirectory: String, patternsToExclude: List, logProvider: ILogProvider) = 13 | let _logger = 14 | log "MetadataBuilder" 15 | |> info "AnalyzeReleaseFile" "Analyze release file: {0}" 16 | |> info "SaveMetadata" "Saving release metadata" 17 | |> info "SavingFiles" "Saving artifacts to update" 18 | |> info "SavingFile" "Adding new file '{0}' as {1}" 19 | |> info "Completed" "Process completed" 20 | |> info "SkipZipEntry" "Skipped entry '{0}' due to forbidden pattern" 21 | |> buildAndAdd logProvider 22 | 23 | let fileBucketDirectory = Path.Combine(workingDirectory, "FileBucket") 24 | 25 | let extractProjectName(releaseFile: String) = 26 | let m = Regex.Match(releaseFile |> Path.GetFileName, "(.+?)[0-9]+(\.[0-9]+)+") 27 | m.Groups.[1].Value.Trim('v').Trim('.') 28 | 29 | let extractVersion(releaseFile: String) = 30 | let m = Regex.Match(releaseFile |> Path.GetFileName, "[0-9]+(\.[0-9]+)+") 31 | m.Value |> Version.Parse 32 | 33 | let readZipEntryContent(name: String, entries: ZipArchiveEntry seq) = 34 | let zipEntry = 35 | entries 36 | |> Seq.find(fun entry -> entry.FullName.Equals(name, StringComparison.OrdinalIgnoreCase)) 37 | 38 | use zipStream = zipEntry.Open() 39 | use memStream = new MemoryStream() 40 | zipStream.CopyTo(memStream) 41 | memStream.ToArray() 42 | 43 | let getVersionFilesSummary(releaseFile: String) = 44 | use fileHandle = File.OpenRead(releaseFile) 45 | 46 | // inspect zip 47 | (new System.IO.Compression.ZipArchive(fileHandle, ZipArchiveMode.Read)).Entries 48 | |> Seq.toArray 49 | |> Array.filter(fun zipEntry -> 50 | let skipZipEntry = 51 | patternsToExclude 52 | |> Seq.exists(fun pattern -> Regex.IsMatch(zipEntry.FullName, pattern)) 53 | 54 | if skipZipEntry then _logger?SkipZipEntry(zipEntry.FullName) 55 | not skipZipEntry 56 | ) 57 | |> Array.map(fun zipEntry -> 58 | // check if it is a directory 59 | if String.IsNullOrEmpty(zipEntry.Name) && (zipEntry.FullName.EndsWith("/") || zipEntry.FullName.EndsWith("\\")) then 60 | (zipEntry.FullName, String.Empty) 61 | else 62 | use zipStream = zipEntry.Open() 63 | use memStream = new MemoryStream() 64 | zipStream.CopyTo(memStream) 65 | (zipEntry.FullName, CryptoUtility.sha256(memStream.ToArray())) 66 | ) 67 | 68 | let saveApplicationMetadata(workingDirectory: String, releaseFile: String, files: (String * String) seq) = 69 | let fileContent = new StringBuilder() 70 | files |> Seq.iter(fun (name, hashValue) -> 71 | fileContent.AppendFormat("{0},{1}", hashValue, name).AppendLine() |> ignore 72 | ) 73 | 74 | // save the content 75 | let versionsDirectory = Path.Combine(workingDirectory, "Versions") 76 | Directory.CreateDirectory(versionsDirectory) |> ignore 77 | let filename = Path.Combine(versionsDirectory, String.Format("{0}.txt", extractVersion(releaseFile))) 78 | File.WriteAllText(filename, fileContent.ToString()) 79 | 80 | let getAllHashPerVersion(workingDirectory: String) = 81 | Directory.GetFiles(Path.Combine(workingDirectory, "Versions")) 82 | |> Array.map(fun filename -> 83 | ( 84 | Path.GetFileNameWithoutExtension(filename), 85 | (File.ReadAllLines(filename) |> Array.map(fun line -> line.Split([|','|]).[0])) 86 | ) 87 | ) 88 | 89 | let saveFilesContent(workingDirectory: String, releaseFile: String, files: (String * String) seq) = 90 | let releaseVersion = extractVersion(releaseFile).ToString() 91 | Directory.CreateDirectory(fileBucketDirectory) |> ignore 92 | 93 | // compute the new files that must be copied 94 | let allHashFiles = 95 | getAllHashPerVersion(workingDirectory) 96 | |> Array.filter(fun (version, _) -> version.Equals(releaseVersion, StringComparison.OrdinalIgnoreCase) |> not) 97 | |> Array.collect(snd) 98 | |> Set.ofArray 99 | 100 | let allVersionHash = files |> Seq.map(fun (_, h) -> h) |> Set.ofSeq 101 | let newHashFiles = Set.difference allVersionHash allHashFiles 102 | 103 | // open the zip file again 104 | use fileHandle = File.OpenRead(releaseFile) 105 | let entries = (new ZipArchive(fileHandle, ZipArchiveMode.Read)).Entries 106 | 107 | // save the new files 108 | files 109 | |> Seq.filter(fun (_, hashValue) -> newHashFiles.Contains(hashValue) ) 110 | |> Seq.iter(fun (name, hashValue) -> 111 | let filename = Path.Combine(fileBucketDirectory, hashValue) 112 | if not(File.Exists(filename)) then 113 | _logger?SavingFile(name, hashValue) 114 | File.WriteAllBytes(filename, readZipEntryContent(name, entries)) 115 | ) 116 | 117 | new(workingDirectory: String) = new MetadataBuilder(workingDirectory, new List()) 118 | new(workingDirectory: String, patternsToExclude: List) = new MetadataBuilder(workingDirectory, patternsToExclude, new LogProvider()) 119 | 120 | member this.CreateReleaseMetadata(releaseFile: String) = 121 | _logger?AnalyzeReleaseFile(Path.GetFileName(releaseFile)) 122 | let files = getVersionFilesSummary(releaseFile) 123 | let projectWorkspace = Path.Combine(workingDirectory, extractProjectName(releaseFile)) 124 | 125 | _logger?SaveMetadata() 126 | saveApplicationMetadata(projectWorkspace, releaseFile, files) 127 | 128 | _logger?SavingFiles() 129 | saveFilesContent(projectWorkspace, releaseFile, files) 130 | 131 | _logger?Completed() 132 | () 133 | -------------------------------------------------------------------------------- /Src/ES.Update.Releaser/paket.references: -------------------------------------------------------------------------------- 1 | ES.Fslog.Core -------------------------------------------------------------------------------- /Src/ES.Update/CryptoUtility.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update 2 | 3 | open System 4 | open System.Security.Cryptography 5 | open System.IO 6 | open Org.BouncyCastle.Crypto.Generators 7 | open Org.BouncyCastle.Crypto.Parameters 8 | open Org.BouncyCastle.Asn1.X9 9 | open Org.BouncyCastle.Security 10 | open Org.BouncyCastle.Pkcs 11 | open Org.BouncyCastle.X509 12 | open System.Net.NetworkInformation 13 | open System.Text 14 | 15 | [] 16 | module CryptoUtility = 17 | [] 18 | let generateKeys() = 19 | let ecSpec = ECNamedCurveTable.GetOid("prime256v1") 20 | let gen = new ECKeyPairGenerator("ECDSA") 21 | let keyGenParam = new ECKeyGenerationParameters(ecSpec, new SecureRandom()) 22 | gen.Init(keyGenParam) 23 | let keyPair = gen.GenerateKeyPair() 24 | 25 | // public key 26 | let publicKey = keyPair.Public :?> ECPublicKeyParameters 27 | let publicKeyBytes = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(publicKey).GetDerEncoded() 28 | 29 | // private key 30 | let privatekey = keyPair.Private :?> ECPrivateKeyParameters 31 | let pkinfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privatekey) 32 | let privatekeyBytes = pkinfo.GetDerEncoded() 33 | 34 | (publicKeyBytes, privatekeyBytes) 35 | 36 | let sign(data: Byte array, privateKey: Byte array) = 37 | let privateKey = PrivateKeyFactory.CreateKey(privateKey) 38 | let signer = SignerUtilities.GetSigner("SHA256withECDSA") 39 | signer.Init(true, privateKey) 40 | signer.BlockUpdate(data, 0, data.Length) 41 | signer.GenerateSignature() 42 | 43 | let hexSign(data: Byte array, privateKey: Byte array) = 44 | BitConverter.ToString(sign(data, privateKey)).Replace("-","") 45 | 46 | let verifyData(data: Byte array, signature: Byte array, publicKey: Byte array) = 47 | let bpubKey = PublicKeyFactory.CreateKey(publicKey) :?> ECPublicKeyParameters 48 | let signer = SignerUtilities.GetSigner("SHA256withECDSA") 49 | signer.Init (false, bpubKey) 50 | signer.BlockUpdate (data, 0, data.Length) 51 | signer.VerifySignature(signature) 52 | 53 | let verifyString(data: String, signature: String, publicKey: Byte array) = 54 | let byteSignature = [| 55 | for i in 0..2..(signature.Length-1) do 56 | let s = String.Format("{0}{1}", signature.[i], signature.[i+1]) 57 | yield Convert.ToByte(s, 16) 58 | |] 59 | 60 | let byteData = Encoding.UTF8.GetBytes(data) 61 | verifyData(byteData, byteSignature, publicKey) 62 | 63 | let sha256Raw(content: Byte array) = 64 | use sha = new SHA256Managed() 65 | sha.ComputeHash(content) 66 | 67 | let sha256(content: Byte array) = 68 | BitConverter.ToString(sha256Raw(content)).Replace("-",String.Empty).ToUpperInvariant() 69 | 70 | let decrypt(data: Byte array, key: Byte array, iv: Byte array) = 71 | use aes = new AesManaged(Key = key, IV = iv, Padding = PaddingMode.ISO10126) 72 | use ms = new MemoryStream(data) 73 | use resultStream = new MemoryStream() 74 | use sr = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read) 75 | sr.CopyTo(resultStream) 76 | sr.Close() 77 | resultStream.ToArray() 78 | 79 | let encrypt(data: Byte array, key: Byte array, iv: Byte array) = 80 | use aes = new AesManaged(Key = key, IV = iv, Padding = PaddingMode.ISO10126) 81 | use ms = new MemoryStream() 82 | use sw = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write) 83 | sw.Write(data, 0, data.Length) 84 | sw.Close() 85 | ms.ToArray() 86 | 87 | let getMacAddresses() = 88 | NetworkInterface.GetAllNetworkInterfaces() 89 | |> Array.filter(fun ni -> 90 | (ni.NetworkInterfaceType = NetworkInterfaceType.Wireless80211 91 | || ni.NetworkInterfaceType = NetworkInterfaceType.Ethernet) 92 | && ni.OperationalStatus = OperationalStatus.Up 93 | ) 94 | |> Array.collect(fun ni -> ni.GetPhysicalAddress().GetAddressBytes()) 95 | 96 | let getHardDiskSerials() = 97 | let bytes = 98 | DriveInfo.GetDrives() 99 | |> Array.filter(fun drive -> drive.IsReady) 100 | |> Array.collect(fun drive -> drive.RootDirectory.CreationTime.ToBinary() |> BitConverter.GetBytes) 101 | |> sha256Raw 102 | Array.sub bytes 0 16 103 | 104 | let encryptKey(data: Byte array) = 105 | let key = getMacAddresses() |> sha256Raw 106 | let iv = getHardDiskSerials() 107 | encrypt(data, key, iv) 108 | 109 | let private decryptKey(data: Byte array) = 110 | let key = getMacAddresses() |> sha256Raw 111 | let iv = getHardDiskSerials() 112 | decrypt(data, key, iv) 113 | 114 | let readPrivateKey(filename: String) = 115 | let encodedKey = Convert.FromBase64String(File.ReadAllText(filename)) 116 | let effectiveKey = decryptKey(encodedKey) 117 | Convert.ToBase64String(effectiveKey) 118 | 119 | let encryptExportedKey(password: String, base64KeyData: String) = 120 | let keyDataBytes = Convert.FromBase64String(base64KeyData) 121 | let passwordBytes = Encoding.UTF8.GetBytes(password) |> sha256Raw 122 | let iv = Array.zeroCreate(16) 123 | use provider = new RNGCryptoServiceProvider() 124 | provider.GetBytes(iv) 125 | let encryptedKey = encrypt(keyDataBytes, passwordBytes, iv) 126 | let encryptedData = Array.concat [iv; encryptedKey] 127 | Convert.ToBase64String(encryptedData) 128 | 129 | let decryptImportedKey(password: String, base64KeyData: String) = 130 | let keyDataBytes = Convert.FromBase64String(base64KeyData) 131 | let passwordBytes = Encoding.UTF8.GetBytes(password) |> sha256Raw 132 | let iv = Array.sub keyDataBytes 0 16 133 | let encryptedKey = keyDataBytes.[16..] 134 | let decryptedKey = decrypt(encryptedKey, passwordBytes, iv) 135 | Convert.ToBase64String(decryptedKey) -------------------------------------------------------------------------------- /Src/ES.Update/ES.Update.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Src/ES.Update/Entities.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update 2 | 3 | open System 4 | 5 | // create this class to easier the C# interoperability 6 | type Result(result: Boolean) = 7 | member val Success = result with get, set 8 | member val Error = String.Empty with get, set 9 | 10 | type ApplicationFile = { 11 | FilePath: String 12 | Hash: String 13 | } -------------------------------------------------------------------------------- /Src/ES.Update/Installer.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update 2 | 3 | open System 4 | open System.IO 5 | open System.IO.Compression 6 | open System.Diagnostics 7 | open System.Text 8 | open System.Threading 9 | open System.Collections.Generic 10 | open System.Text.RegularExpressions 11 | open ES.Fslog 12 | 13 | module AbbandonedMutex = 14 | let mutable mutex: Mutex option = None 15 | 16 | type Installer(destinationDirectory: String, logProvider: ILogProvider) as this = 17 | let _logger = 18 | log "Installer" 19 | |> info "ZipExtracted" "Update zip extracted to: {0}" 20 | |> info "FilesCopied" "All update files were copied to: {0}" 21 | |> info "RunInstaller" "Run installer: {0} {1}" 22 | |> info "NoInstaller" "No installer found in directory '{0}'. Copy files to '{1}'" 23 | |> verbose "CopyFile" "Copy '{0}' to '{1}'" 24 | |> verbose "MoveFile" "Move existing file '{0}' to '{1}'" 25 | |> verbose "SkipFile" "Skip file due to forbidden pattern: {0}" 26 | |> verbose "SkipFileSameHash" "Skip file becasue same hash: {0}" 27 | |> verbose "InstallerNotFound" "Installer {0} not found" 28 | |> critical "InstallerIntegrityFail" "The integrity check of the installer failed" 29 | |> buildAndAdd logProvider 30 | 31 | let compareHash(hashValue: String, content: Byte array) = 32 | let computedHashValue = CryptoUtility.sha256(content) 33 | hashValue.Equals(computedHashValue, StringComparison.OrdinalIgnoreCase) 34 | 35 | let moveFile(destinationFile: String) = 36 | let oldFilesDir = Path.Combine(destinationDirectory, "OLD-files") 37 | Directory.CreateDirectory(oldFilesDir) |> ignore 38 | let copyFile = Path.Combine(oldFilesDir, Path.GetFileName(destinationFile)) 39 | if not <| File.Exists(copyFile) then 40 | _logger?MoveFile(destinationFile, copyFile) 41 | File.Move(destinationFile, copyFile) 42 | 43 | let copyFile(filePath: String, contantHashValue: String, content: Byte array) = 44 | let destinationFile = Path.Combine(destinationDirectory, filePath) 45 | Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)) |> ignore 46 | if File.Exists(destinationFile) then 47 | // check if same hash, if so, skip the file move 48 | if not <| sha256(File.ReadAllBytes(destinationFile)).Equals(contantHashValue) then 49 | moveFile(destinationFile) 50 | _logger?CopyFile(filePath, destinationFile) 51 | else 52 | File.WriteAllBytes(destinationFile, content) 53 | _logger?CopyFile(filePath, destinationFile) 54 | 55 | let getFiles(fileList: String) = 56 | fileList.Split([|'\r'; '\n'|], StringSplitOptions.RemoveEmptyEntries) 57 | |> Array.filter(String.IsNullOrWhiteSpace >> not) 58 | |> Array.map(fun line -> 59 | let items = line.Split([|','|]) 60 | (items.[0], String.Join(",", items.[1..])) 61 | ) 62 | 63 | let verifyIntegrity(directory: String, catalogFileName: String) = 64 | // read catalog 65 | let catalog = File.ReadAllText(Path.Combine(directory, catalogFileName)) 66 | let files = getFiles(catalog) 67 | 68 | // check integrity 69 | let mutable integrityFailedOnFile = String.Empty 70 | files 71 | |> Array.filter(fun (hashValue, _) -> not(String.IsNullOrWhiteSpace(hashValue))) 72 | |> Array.forall(fun (hashValue, filePath) -> 73 | // try to read the file content via hash or file name (in case of installer) 74 | let fullFileName = 75 | let tmp = Path.Combine(directory, hashValue) 76 | if File.Exists(tmp) then tmp 77 | else Path.Combine(directory, filePath) 78 | 79 | let content = File.ReadAllBytes(fullFileName) 80 | if compareHash(hashValue, content) then 81 | true 82 | else 83 | integrityFailedOnFile <- filePath 84 | false 85 | ) 86 | |> fun result -> 87 | if result then Ok () 88 | else 89 | _logger?InstallerIntegrityFail() 90 | Error("Integrity check failed on file: " + integrityFailedOnFile) 91 | 92 | let extractZip(zipArchive: ZipArchive, destinationDirectory: String) = 93 | Directory.CreateDirectory(destinationDirectory) |> ignore 94 | 95 | zipArchive.Entries 96 | |> Seq.iter(fun entry -> 97 | let fileName = Path.Combine(destinationDirectory, entry.FullName) 98 | Directory.CreateDirectory(Path.GetDirectoryName(fileName)) |> ignore 99 | let content = Utility.readEntry(zipArchive, entry.FullName) 100 | File.WriteAllBytes(fileName, content) 101 | ) 102 | 103 | destinationDirectory 104 | 105 | let isOkToCopy(patternsSkipOnExist: List) (hashValueFile: String, filePath: String) = 106 | if File.Exists(filePath) && patternsSkipOnExist.Count > 0 then 107 | // is forbidden pattern 108 | let skipFile = 109 | patternsSkipOnExist 110 | |> Seq.exists(fun pattern -> Regex.IsMatch(filePath, pattern)) 111 | 112 | if skipFile then _logger?SkipFile(filePath) 113 | not skipFile 114 | else 115 | true 116 | 117 | let copyAllFiles(extractedDirectory: String, files: (String * String) array, patternsSkipOnExist: List) = 118 | files 119 | |> Array.filter(isOkToCopy patternsSkipOnExist) 120 | |> Array.iter(fun (hashValue, filePath) -> 121 | let sourceFilePath = Path.Combine(extractedDirectory, hashValue) 122 | if File.Exists(sourceFilePath) then 123 | let content = File.ReadAllBytes(sourceFilePath) 124 | copyFile(filePath, hashValue, content) 125 | ) 126 | 127 | let createInstallerMutex(argumentString: String) = 128 | let mutexName = 129 | Regex.Replace(argumentString, "[^a-zA-Z]+", String.Empty) 130 | |> Encoding.UTF8.GetBytes 131 | |> sha256 132 | 133 | AbbandonedMutex.mutex <- Some <| new Mutex(true, mutexName) 134 | 135 | let runInstaller(processInfo: ProcessStartInfo) = 136 | try 137 | createInstallerMutex(processInfo.Arguments) 138 | _logger?RunInstaller(processInfo.FileName, processInfo.Arguments) 139 | Process.Start(processInfo) |> ignore 140 | Ok () 141 | with e -> 142 | Error(e.ToString()) 143 | 144 | let buildProcessObject(extractedDirectory: String) = 145 | let isVerbose = 146 | logProvider.GetLoggers() 147 | |> Seq.exists(fun logger -> logger.Level = LogLevel.Verbose) 148 | 149 | let baseArgumentString = new StringBuilder() 150 | baseArgumentString.AppendFormat("--source \"{0}\" --dest \"{1}\"", extractedDirectory, destinationDirectory) |> ignore 151 | 152 | if isVerbose then 153 | baseArgumentString.Append(" --verbose") |> ignore 154 | 155 | if not this.RemoveTempFile then 156 | baseArgumentString.Append(" --no-clean") |> ignore 157 | 158 | let dllInstaller = Path.Combine(extractedDirectory, "Installer.dll") 159 | if not <| File.Exists(dllInstaller) then 160 | _logger?InstallerNotFound(dllInstaller) 161 | 162 | let exeInstaller = Path.Combine(extractedDirectory, "Installer.exe") 163 | if not <| File.Exists(dllInstaller) then 164 | _logger?InstallerNotFound(exeInstaller) 165 | 166 | if File.Exists(dllInstaller) then 167 | Some("dotnet", String.Format("\"{0}\" {1}", dllInstaller, baseArgumentString.ToString())) 168 | elif File.Exists(exeInstaller) then 169 | Some(exeInstaller, baseArgumentString.ToString()) 170 | else None 171 | |> function 172 | | Some (installer, argumentString) -> 173 | new ProcessStartInfo( 174 | FileName = installer, 175 | UseShellExecute = false, 176 | Arguments = argumentString 177 | ) 178 | |> Some 179 | | None -> 180 | _logger?NoInstaller(extractedDirectory, destinationDirectory) 181 | None 182 | 183 | let runVerifiedInstaller(processInfo, extractedDirectory: String) = 184 | match verifyIntegrity(extractedDirectory, "installer-catalog") with 185 | | Ok _ -> runInstaller(processInfo) 186 | | Error e -> Error e 187 | 188 | member private this.DoInstall(extractedDirectory: String, fileList: String, patternsSkipOnExist: List) = 189 | match buildProcessObject(extractedDirectory) with 190 | | Some processInfo -> 191 | if this.SkipIntegrityCheck 192 | then runInstaller(processInfo) 193 | else runVerifiedInstaller(processInfo, extractedDirectory) 194 | | None -> 195 | let files = getFiles(fileList) 196 | copyAllFiles(extractedDirectory, files, patternsSkipOnExist) 197 | _logger?FilesCopied(destinationDirectory) 198 | 199 | // cleanup spurious files 200 | if this.RemoveTempFile then Directory.Delete(extractedDirectory, true) 201 | Ok () 202 | 203 | member val PatternsSkipOnExist = new List() with get, set 204 | member val SkipIntegrityCheck = false with get, set 205 | member val RemoveTempFile = true with get, set 206 | 207 | member this.CopyUpdates(sourceDirectory: String) = 208 | let catalog = File.ReadAllText(Path.Combine(sourceDirectory, "catalog")) 209 | let files = getFiles(catalog) 210 | copyAllFiles(sourceDirectory, files, this.PatternsSkipOnExist) 211 | 212 | member internal this.InstallUpdate(directory: String, fileList: String) = 213 | match verifyIntegrity(directory, "catalog") with 214 | | Ok _ -> this.DoInstall(directory, fileList, this.PatternsSkipOnExist) 215 | | Error e -> Error e 216 | 217 | member internal this.InstallUpdate(zipArchive: ZipArchive, fileList: String) = 218 | let tempDir = Path.Combine(Path.GetTempPath(), fileList |> Encoding.UTF8.GetBytes |> sha256) 219 | let extractedDirectory = extractZip(zipArchive, tempDir) 220 | _logger?ZipExtracted(extractedDirectory) 221 | this.InstallUpdate(extractedDirectory, fileList) -------------------------------------------------------------------------------- /Src/ES.Update/Updater.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.Net 6 | open System.IO 7 | open System.IO.Compression 8 | open System.Text 9 | open ES.Fslog 10 | 11 | type Updater(serverUri: Uri, projectName: String, currentVersion: Version, destinationDirectory: String, publicKey: Byte array, logProvider: ILogProvider) as this = 12 | let _downloadingFileEvent = new Event() 13 | let _downloadedFileEvent = new Event() 14 | let mutable _additionalData: Dictionary option = None 15 | 16 | let _logger = 17 | log "Updater" 18 | |> critical "CatalogIntegrityFail" "The integrity check of the catalog failed" 19 | |> critical "DownloadError" "Download error: {0}" 20 | |> info "DownloadDone" "Updates downloaded to file: {0}" 21 | |> buildAndAdd logProvider 22 | 23 | let getContent(url: String) = 24 | try 25 | // configure request 26 | let webRequest = WebRequest.Create(new Uri(serverUri, url)) :?> HttpWebRequest 27 | webRequest.Method <- "POST" 28 | webRequest.Timeout <- 5 * 60 * 1000 29 | webRequest.ContentType <- "application/x-www-form-urlencoded" 30 | 31 | // compose data 32 | let data = new StringBuilder() 33 | data.AppendFormat("version={0}&project={1}", currentVersion, projectName) |> ignore 34 | 35 | _additionalData 36 | |> Option.iter(fun additionalData -> 37 | additionalData 38 | |> Seq.iter(fun kv -> 39 | data.AppendFormat("&{0}={1}", kv.Key, kv.Value) |> ignore 40 | ) 41 | ) 42 | 43 | // write data 44 | use streamWriter = new StreamWriter(webRequest.GetRequestStream()) 45 | streamWriter.Write(data.ToString().Trim('&')) 46 | streamWriter.Close() 47 | 48 | // send the request and save the response to file 49 | use webResponse = webRequest.GetResponse() :?> HttpWebResponse 50 | use responseStream = webResponse.GetResponseStream() 51 | use memoryStream = new MemoryStream() 52 | responseStream.CopyTo(memoryStream) 53 | memoryStream.ToArray() 54 | with e -> 55 | _logger?DownloadError(e.Message) 56 | Array.empty 57 | 58 | let getString(url: String) = 59 | Encoding.UTF8.GetString(getContent(url)) 60 | 61 | let parseCatalog(catalog: String) = 62 | catalog.Split("\r\n") 63 | |> Array.filter(fun line -> String.IsNullOrWhiteSpace(line) |> not) 64 | |> Array.map(fun line -> 65 | // hash, file path 66 | let items = line.Split(",") 67 | {Hash = items.[0]; FilePath = items.[1]} 68 | ) 69 | 70 | let tryGetCatalog() = 71 | let catalog = getString("updates") 72 | let items = catalog.Split("\r\n") 73 | if items.Length >= 2 then 74 | let signature = items.[0].Trim() 75 | let catalog = String.Join("\r\n", items.[1..]) 76 | if this.SkipIntegrityCheck || CryptoUtility.verifyString(catalog, signature, publicKey) then 77 | (String.Empty, Some(parseCatalog(catalog))) 78 | else 79 | ("Wrong catalog signature", None) 80 | else 81 | ("Wrong catalog format", None) 82 | 83 | let isFileAlreadyDownloaded(hash: String, filePath: String) = 84 | if File.Exists(filePath) then 85 | let receivedHash = CryptoUtility.sha256(File.ReadAllBytes(filePath)) 86 | receivedHash.Equals(hash, StringComparison.OrdinalIgnoreCase) 87 | else 88 | false 89 | 90 | let downloadFile(file: ApplicationFile) = 91 | _downloadingFileEvent.Trigger(file.Hash) 92 | let storagePath = Path.Combine(destinationDirectory, file.FilePath) 93 | let fileContent = getContent(String.Format("file/{0}", file.Hash)) 94 | 95 | if fileContent |> Array.isEmpty then 96 | new Result(false, Error = String.Format("Error downloading file: {0}", file.FilePath)) 97 | else 98 | let receivedHash = CryptoUtility.sha256(fileContent) 99 | if receivedHash.Equals(file.Hash, StringComparison.OrdinalIgnoreCase) then 100 | Directory.CreateDirectory(Path.GetDirectoryName(storagePath)) |> ignore 101 | File.WriteAllBytes(storagePath, fileContent) 102 | _downloadedFileEvent.Trigger(file.Hash) 103 | new Result(true) 104 | else 105 | new Result(false, Error = String.Format("Downloaded file '{0}' has a wrong hash", file.FilePath)) 106 | 107 | let verifyCatalogContentIntegrity(catalog: Byte array, installerCatalog: (Byte array) option, signature: Byte array, installerSignature: (Byte array) option) = 108 | let catalogOk = CryptoUtility.verifyData(catalog, signature, publicKey) 109 | let installerCatalogOk = 110 | match (installerCatalog, installerSignature) with 111 | | (Some installerCatalog, Some installerSignature) -> 112 | CryptoUtility.verifyData(installerCatalog, installerSignature, publicKey) 113 | | (Some _, None) -> false 114 | | (None, Some _) -> false 115 | | _ -> true 116 | catalogOk && installerCatalogOk 117 | 118 | let verifyCatalogIntegrity(zipArchive: ZipArchive) = 119 | let catalog = Utility.readEntry(zipArchive, "catalog") 120 | let signature = Utility.readEntry(zipArchive, "signature") 121 | let installerCatalog = Utility.tryReadEntry(zipArchive, "installer-catalog") 122 | let installerSignature = Utility.tryReadEntry(zipArchive, "installer-signature") 123 | verifyCatalogContentIntegrity(catalog, installerCatalog, signature, installerSignature) 124 | 125 | let downloadFiles(files: ApplicationFile array) = 126 | files 127 | |> Array.map(downloadFile) 128 | |> Array.tryFind(fun res -> not res.Success) 129 | 130 | let getNewFiles() = 131 | match tryGetCatalog() with 132 | | (_, Some files) -> 133 | let newFiles = 134 | files 135 | |> Array.filter(fun fi -> 136 | let storagePath = Path.Combine(destinationDirectory, fi.FilePath) 137 | isFileAlreadyDownloaded(fi.Hash, storagePath) |> not 138 | ) 139 | (String.Empty, Some newFiles) 140 | | r -> r 141 | 142 | new (serverUri: Uri, projectName: String, currentVersion: Version, destinationDirectory: String, publicKey: Byte array) = new Updater(serverUri, projectName, currentVersion, destinationDirectory, publicKey, LogProvider.GetDefault()) 143 | 144 | member val PatternsSkipOnExist = new List() with get, set 145 | member val SkipIntegrityCheck = false with get, set 146 | member val RemoveTempFile = true with get, set 147 | 148 | [] 149 | member val DownloadingFile = _downloadingFileEvent.Publish 150 | 151 | [] 152 | member val DownloadedFile = _downloadedFileEvent.Publish 153 | 154 | member this.AddParameter(name: String, value: String) = 155 | let dataStorage = 156 | match _additionalData with 157 | | None -> 158 | _additionalData <- new Dictionary() |> Some 159 | _additionalData.Value 160 | | Some d -> d 161 | dataStorage.[name] <- value 162 | 163 | member internal this.InstallUpdatesFromFile(file: String, installer: Installer) = 164 | use zipStream = File.OpenRead(file) 165 | use zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read) 166 | if this.SkipIntegrityCheck || verifyCatalogIntegrity(zipArchive) then 167 | let fileList = Utility.readEntry(zipArchive, "catalog") 168 | installer.InstallUpdate(zipArchive, Encoding.UTF8.GetString(fileList)) 169 | else 170 | _logger?CatalogIntegrityFail() 171 | Error "Integrity check failed" 172 | 173 | member internal this.InstallUpdatesFromDirectory(directory: String, installer: Installer) = 174 | let fileList = Path.Combine(directory, "catalog") |> File.ReadAllText 175 | let mutable integrityCheckOk = true 176 | 177 | // check integrity 178 | if not this.SkipIntegrityCheck then 179 | let catalog = Path.Combine(directory, "catalog") |> File.ReadAllBytes 180 | let signature = Path.Combine(directory, "signature") |> File.ReadAllBytes 181 | let installerCatalogFile = Path.Combine(directory, "installer-catalog") 182 | let installerSignatureFile = Path.Combine(directory, "installer-signature") 183 | 184 | if File.Exists(installerCatalogFile) && File.Exists(installerSignatureFile) then 185 | let installerCatalog = File.ReadAllBytes(installerCatalogFile) 186 | let installerSignature = File.ReadAllBytes(installerSignatureFile) 187 | integrityCheckOk <- verifyCatalogContentIntegrity(catalog, Some installerCatalog, signature, Some installerSignature) 188 | elif not(File.Exists(installerCatalogFile)) && not(File.Exists(installerSignatureFile)) then 189 | integrityCheckOk <- verifyCatalogContentIntegrity(catalog, None, signature, None) 190 | else 191 | // do I have an installer and not associated signature or the opposite 192 | integrityCheckOk <- false 193 | 194 | if integrityCheckOk then 195 | installer.InstallUpdate(directory, fileList) 196 | else 197 | _logger?CatalogIntegrityFail() 198 | Error "Integrity check failed" 199 | 200 | member this.InstallUpdates(fileOrDirectory: String) = 201 | let installer = 202 | new Installer( 203 | destinationDirectory, 204 | logProvider, 205 | PatternsSkipOnExist = this.PatternsSkipOnExist, 206 | SkipIntegrityCheck = this.SkipIntegrityCheck 207 | ) 208 | 209 | if Directory.Exists(fileOrDirectory) then 210 | this.InstallUpdatesFromDirectory(fileOrDirectory, installer) 211 | elif File.Exists(fileOrDirectory) then 212 | this.InstallUpdatesFromFile(fileOrDirectory, installer) 213 | else 214 | Error(String.Format("{0} is not a valid update path", fileOrDirectory)) 215 | 216 | member this.GetLatestVersion() = 217 | use webClient = new WebClient() 218 | let latestVersionUri = new Uri(serverUri, String.Format("latest?project={0}", projectName)) 219 | webClient.DownloadString(latestVersionUri) |> Version.Parse 220 | 221 | member this.GetNewFiles() = 222 | match getNewFiles() with 223 | | (_, Some files) -> files 224 | | (error, _) -> Array.empty 225 | 226 | member this.Update() = 227 | // prepare update file 228 | let resultDirectory = Path.Combine(Path.GetTempPath(), projectName) 229 | Directory.CreateDirectory(resultDirectory) |> ignore 230 | 231 | // download catalog 232 | match getNewFiles() with 233 | | (_, Some files) -> 234 | match downloadFiles(files) with 235 | | Some error -> error 236 | | None -> new Result(true) 237 | | (error, _) -> 238 | new Result(false, Error = error) -------------------------------------------------------------------------------- /Src/ES.Update/Utility.fs: -------------------------------------------------------------------------------- 1 | namespace ES.Update 2 | 3 | open System 4 | open System.IO.Compression 5 | open System.IO 6 | 7 | module Utility = 8 | let tryReadEntry(zipArchive: ZipArchive, name: String) = 9 | let entry = 10 | zipArchive.Entries 11 | |> Seq.tryFind(fun entry -> entry.FullName.Equals(name, StringComparison.OrdinalIgnoreCase)) 12 | 13 | match entry with 14 | | Some entry -> 15 | use zipStream = entry.Open() 16 | use memStream = new MemoryStream() 17 | zipStream.CopyTo(memStream) 18 | Some <| memStream.ToArray() 19 | | None -> 20 | None 21 | 22 | let readEntry(zipArchive: ZipArchive, name: String) = 23 | tryReadEntry(zipArchive, name).Value -------------------------------------------------------------------------------- /Src/ES.Update/paket.references: -------------------------------------------------------------------------------- 1 | ES.Fslog.Core 2 | BouncyCastle -------------------------------------------------------------------------------- /Src/Examples/Example1/Client.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using ES.Update; 4 | 5 | namespace Example1 6 | { 7 | public class Client 8 | { 9 | public void Run(Server server, String destinationDirectory) 10 | { 11 | var myVersion = Helpers.GetCurrentVersion(); 12 | var updater = new Updater(server.BindingUri, "MyApplication", myVersion, destinationDirectory, server.PublicKey); 13 | 14 | var latestVersion = updater.GetLatestVersion(); 15 | Console.WriteLine("My version: {0}. Latest version: {1}", myVersion, latestVersion); 16 | 17 | if (latestVersion > myVersion) 18 | { 19 | // start update 20 | updater.DownloadingFile += (args, file) => { Console.WriteLine("[-] Downloading: {0}", file); }; 21 | updater.DownloadedFile += (args, file) => { Console.WriteLine("[+] Downloaded: {0}", file); }; 22 | 23 | var updateResult = updater.Update(); 24 | if (updateResult.Success) 25 | { 26 | var fileContent = File.ReadAllText(Path.Combine(destinationDirectory, "folder", "file7.txt")); 27 | Console.WriteLine("Update installed correctly! {0}", fileContent); 28 | Helpers.SaveVersion(latestVersion); 29 | } 30 | else 31 | { 32 | Console.WriteLine("Error during installing updates: {0}", updateResult.Error); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Src/Examples/Example1/Example1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Src/Examples/Example1/Helpers.cs: -------------------------------------------------------------------------------- 1 | using ES.Update.Releaser; 2 | using System; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Example1 11 | { 12 | public static class Helpers 13 | { 14 | private static String GetVersionFileName() 15 | { 16 | var curDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); 17 | var versionFileName = Path.Combine(curDirectory, Assembly.GetEntryAssembly().ManifestModule.Name + ".version"); 18 | return versionFileName; 19 | } 20 | 21 | public static Version GetCurrentVersion() 22 | { 23 | if (File.Exists(GetVersionFileName())) 24 | { 25 | return Version.Parse(File.ReadAllText(GetVersionFileName())); 26 | } 27 | else 28 | { 29 | return new Version(3, 0); 30 | } 31 | } 32 | 33 | public static void SaveVersion(Version version) 34 | { 35 | File.WriteAllText(GetVersionFileName(), version.ToString()); 36 | } 37 | 38 | private static void CreateFakeReleaseFile(String fileName, Int32 numberOfItems) 39 | { 40 | using (var fileHandle = File.OpenWrite(fileName)) 41 | using (var zipArchive = new ZipArchive(fileHandle, ZipArchiveMode.Create)) 42 | { 43 | for (var i = 0; i < numberOfItems; i++) 44 | { 45 | var entryName = 46 | (i > numberOfItems / 2) ? 47 | Path.Combine("folder", String.Format("file{0}.txt", i)): 48 | String.Format("file{0}.txt", i); 49 | 50 | var entryContent = Encoding.UTF8.GetBytes(String.Format("Content of file: {0}", entryName)); 51 | var entry = zipArchive.CreateEntry(entryName); 52 | using (var entryStream = entry.Open()) 53 | { 54 | entryStream.Write(entryContent, 0, entryContent.Length); 55 | } 56 | } 57 | } 58 | } 59 | 60 | public static (String, String) CreateEnvironment(Int32 numOfFiles = 5) 61 | { 62 | if (File.Exists(GetVersionFileName())) 63 | { 64 | var deleteFile = GetVersionFileName() + ".DELETE"; 65 | if (File.Exists(deleteFile)) 66 | { 67 | // second run 68 | File.Delete(GetVersionFileName()); 69 | File.Delete(deleteFile); 70 | } 71 | else 72 | { 73 | // first run, create delete file for next run 74 | File.WriteAllText(deleteFile, "DELETE ME"); 75 | } 76 | } 77 | 78 | // create dirs 79 | var workspaceDirectory = Path.Combine(Path.GetTempPath(), "TEST_ProgramUpdater_Examples"); 80 | if (Directory.Exists(workspaceDirectory)) 81 | { 82 | Directory.Delete(workspaceDirectory, true); 83 | } 84 | Directory.CreateDirectory(workspaceDirectory); 85 | 86 | var destinationDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "MyApplication"); 87 | if (Directory.Exists(destinationDirectory)) 88 | { 89 | Directory.Delete(destinationDirectory, true); 90 | } 91 | Directory.CreateDirectory(destinationDirectory); 92 | 93 | // create some fake zip File and build metadata 94 | var metadataBuilder = new MetadataBuilder(workspaceDirectory); 95 | var currentDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); 96 | for (var i = 0; i < numOfFiles; i++) 97 | { 98 | var fileName = Path.Combine(currentDir, String.Format("MyApplication.v{0}.0.zip", i + 1)); 99 | Helpers.CreateFakeReleaseFile(fileName, 5 + i); 100 | metadataBuilder.CreateReleaseMetadata(fileName); 101 | File.Delete(fileName); 102 | } 103 | 104 | return (workspaceDirectory, destinationDirectory); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Src/Examples/Example1/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Example1 7 | { 8 | public static class Program 9 | { 10 | private static Server _server = null; 11 | 12 | private static void RunServer(String workspaceDirectory) 13 | { 14 | _server = new Server(workspaceDirectory); 15 | 16 | Task.Factory.StartNew(() => 17 | { 18 | _server.Start(); 19 | }); 20 | 21 | var req = (HttpWebRequest)WebRequest.Create(_server.BindingUri); 22 | HttpWebResponse resp = null; 23 | 24 | do 25 | { 26 | try 27 | { 28 | resp = (HttpWebResponse)req.GetResponse(); 29 | } 30 | catch { } 31 | } while (resp != null && resp.StatusCode != HttpStatusCode.OK); 32 | 33 | } 34 | 35 | private static void RunClient(String destinationDirectory) 36 | { 37 | var client = new Client(); 38 | client.Run(_server, destinationDirectory); 39 | } 40 | 41 | static void Main(string[] args) 42 | { 43 | var (workspaceDirectory, destinationDirectory) = Helpers.CreateEnvironment(4); 44 | RunServer(workspaceDirectory); 45 | RunClient(destinationDirectory); 46 | _server.Stop(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Src/Examples/Example1/Server.cs: -------------------------------------------------------------------------------- 1 | using ES.Fslog; 2 | using ES.Fslog.Loggers; 3 | using ES.Fslog.TextFormatters; 4 | using ES.Update; 5 | using ES.Update.Backend; 6 | using System; 7 | using System.IO; 8 | using System.Reflection; 9 | 10 | namespace Example1 11 | { 12 | public class Server 13 | { 14 | public Uri BindingUri { get; set; } 15 | public Byte[] PublicKey { get; set; } 16 | public String WorkspaceDirectory { get; set; } 17 | 18 | public Server(String workspaceDirectory) 19 | { 20 | var logProvider = new LogProvider(); 21 | logProvider.AddLogger(new ConsoleLogger(LogLevel.Verbose, new ConsoleLogFormatter())); 22 | 23 | this.WorkspaceDirectory = workspaceDirectory; 24 | var (publicKey, privateKey) = CryptoUtility.GenerateKeys(); 25 | this.PublicKey = publicKey; 26 | 27 | var rnd = new Random(); 28 | this.BindingUri = new Uri(String.Format("http://127.0.0.1:{0}", rnd.Next(1025, 65534))); 29 | this.WebServer = new WebServer(this.BindingUri, this.WorkspaceDirectory, privateKey, logProvider); 30 | } 31 | 32 | public WebServer WebServer { get; set; } 33 | 34 | public void Start() 35 | { 36 | this.WebServer.Start(); 37 | } 38 | 39 | public void Stop() 40 | { 41 | this.WebServer.Stop(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Src/Examples/Example1/paket.references: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /Src/Examples/Example3/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Src/Examples/Example3/AuthenticatedWebServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using ES.Update; 7 | using ES.Update.Backend; 8 | using static Suave.Http; 9 | 10 | namespace Example3 11 | { 12 | public class AuthenticatedWebServer : WebServer 13 | { 14 | public static Byte[] PrivateKey = null; 15 | public static Byte[] PublicKey = null; 16 | public static Uri BindingUri = null; 17 | public static String Username = "admin"; 18 | public static String Password = "password"; 19 | 20 | static AuthenticatedWebServer() 21 | { 22 | var (publicKey, privateKey) = CryptoUtility.GenerateKeys(); 23 | PrivateKey = privateKey; 24 | PublicKey = publicKey; 25 | 26 | var rnd = new Random(); 27 | BindingUri = new Uri(String.Format("http://127.0.0.1:{0}", rnd.Next(1025, 65534))); 28 | } 29 | 30 | public AuthenticatedWebServer(String workspaceDirectory) : 31 | base(BindingUri, workspaceDirectory, PrivateKey) 32 | { 33 | // specify a prefix to use for the uri composition 34 | this.PathPrefix = "/myupdate"; 35 | } 36 | 37 | public override Boolean Authenticate(HttpContext ctx) 38 | { 39 | if (ctx.request.method.IsPOST) 40 | { 41 | var formParameters = Encoding.UTF8.GetString(ctx.request.rawForm).Split('&'); 42 | var username = String.Empty; 43 | var password = String.Empty; 44 | 45 | foreach(var parameter in formParameters) 46 | { 47 | var nameValue = parameter.Split('='); 48 | if (nameValue[0].Equals("Username", StringComparison.OrdinalIgnoreCase)) 49 | { 50 | username = nameValue[1]; 51 | } 52 | else if (nameValue[0].Equals("Password", StringComparison.OrdinalIgnoreCase)) 53 | { 54 | password = nameValue[1]; 55 | } 56 | } 57 | 58 | return 59 | username.Equals(AuthenticatedWebServer.Username, StringComparison.Ordinal) 60 | && password.Equals(AuthenticatedWebServer.Password, StringComparison.Ordinal); 61 | } 62 | else 63 | { 64 | return true; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Src/Examples/Example3/Client.cs: -------------------------------------------------------------------------------- 1 | using ES.Update; 2 | using Example2; 3 | using System; 4 | using System.IO; 5 | 6 | namespace Example3 7 | { 8 | public class Client 9 | { 10 | public void Run(String destinationDirectory) 11 | { 12 | var myVersion = Helpers.GetCurrentVersion(); 13 | var serverUri = new Uri(AuthenticatedWebServer.BindingUri, "myupdate/"); 14 | var updater = new Updater(serverUri, "MyApplication", myVersion, destinationDirectory, AuthenticatedWebServer.PublicKey); 15 | 16 | // add username and password to the update request 17 | updater.AddParameter("username", AuthenticatedWebServer.Username); 18 | updater.AddParameter("password", AuthenticatedWebServer.Password); 19 | 20 | var latestVersion = updater.GetLatestVersion(); 21 | Console.WriteLine("My version: {0}. Latest version: {1}", myVersion, latestVersion); 22 | 23 | if (latestVersion > myVersion) 24 | { 25 | // start update 26 | var updateResult = updater.Update(myVersion); 27 | if (updateResult.Success) 28 | { 29 | var fileContent = File.ReadAllText(Path.Combine(destinationDirectory, "folder", "file8.txt")); 30 | Console.WriteLine("Update installed correctly! {0}", fileContent); 31 | Helpers.SaveVersion(latestVersion); 32 | } 33 | else 34 | { 35 | Console.WriteLine("Error during installing updates: {0}", updateResult.Error); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Src/Examples/Example3/Example3.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50} 8 | Exe 9 | Example3 10 | Example3 11 | v4.7 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {6176038c-5a8c-4117-81cd-5a936b0679ea} 58 | Example2 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ..\..\packages\FSharp.Core\lib\net45\FSharp.Core.dll 67 | True 68 | True 69 | 70 | 71 | 72 | 73 | 74 | 75 | ..\..\packages\FSharp.Core\lib\netstandard2.0\FSharp.Core.dll 76 | True 77 | True 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ..\..\packages\Suave\lib\net461\Suave.dll 87 | True 88 | True 89 | 90 | 91 | 92 | 93 | 94 | 95 | ..\..\packages\Suave\lib\netstandard2.0\Suave.dll 96 | True 97 | True 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Src/Examples/Example3/Program.cs: -------------------------------------------------------------------------------- 1 | using Example2; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Example3 7 | { 8 | class Program 9 | { 10 | private static AuthenticatedWebServer _server = null; 11 | 12 | private static void RunServer(String workspaceDirectory) 13 | { 14 | var wait = new ManualResetEventSlim(); 15 | Task.Factory.StartNew(() => 16 | { 17 | _server = new AuthenticatedWebServer(workspaceDirectory); 18 | wait.Set(); 19 | _server.Start(); 20 | }); 21 | wait.Wait(); 22 | Thread.Sleep(2000); 23 | } 24 | 25 | private static void RunClient(String destinationDirectory) 26 | { 27 | var client = new Client(); 28 | client.Run(destinationDirectory); 29 | } 30 | 31 | static void Main(string[] args) 32 | { 33 | var (workspaceDirectory, destinationDirectory) = Helpers.CreateEnvironment(5); 34 | RunServer(workspaceDirectory); 35 | RunClient(destinationDirectory); 36 | _server.Stop(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Src/Examples/Example3/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Example3")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Example3")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("3cb4c2c6-2e3d-4dda-95bf-67cb19dc0f50")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Src/Examples/Example3/paket.references: -------------------------------------------------------------------------------- 1 | Suave -------------------------------------------------------------------------------- /Src/Examples/Example4/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Src/Examples/Example4/Client.cs: -------------------------------------------------------------------------------- 1 | using ES.Update; 2 | using Example2; 3 | using System; 4 | 5 | namespace Example4 6 | { 7 | public class Client 8 | { 9 | public void Run(Server server, String destinationDirectory) 10 | { 11 | var myVersion = Helpers.GetCurrentVersion(); 12 | var updater = new Updater(server.BindingUri, "MyApplication", myVersion, destinationDirectory, server.PublicKey); 13 | 14 | var latestVersion = updater.GetLatestVersion(); 15 | Console.WriteLine("My version: {0}. Latest version: {1}", myVersion, latestVersion); 16 | 17 | if (latestVersion > myVersion) 18 | { 19 | // start update 20 | var updateResult = updater.Update(myVersion); 21 | if (updateResult.Success) 22 | { 23 | Console.WriteLine("Everything is fine the installer program is now running. After completation you should see in this directory a file named 'file8.txt' in directory 'folder'"); 24 | Helpers.SaveVersion(latestVersion); 25 | } 26 | else 27 | { 28 | Console.WriteLine("Error during installing updates: {0}", updateResult.Error); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Src/Examples/Example4/Example4.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF} 8 | Exe 9 | Example4 10 | Example4 11 | v4.7 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {6176038c-5a8c-4117-81cd-5a936b0679ea} 56 | Example2 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Src/Examples/Example4/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Example2; 8 | 9 | namespace Example4 10 | { 11 | public static class Program 12 | { 13 | private static Server _server = null; 14 | 15 | private static void RunServer(String workspaceDirectory) 16 | { 17 | var wait = new ManualResetEventSlim(); 18 | Task.Factory.StartNew(() => 19 | { 20 | _server = new Server(workspaceDirectory); 21 | 22 | // set the installer path 23 | var installerPath = Path.GetDirectoryName(typeof(Installer.Program).Assembly.Location); 24 | _server.WebServer.InstallerPath = installerPath; 25 | 26 | wait.Set(); 27 | _server.Start(); 28 | }); 29 | wait.Wait(); 30 | Thread.Sleep(2000); 31 | } 32 | 33 | private static void RunClient(String destinationDirectory) 34 | { 35 | var client = new Client(); 36 | client.Run(_server, destinationDirectory); 37 | } 38 | 39 | static void Main(string[] args) 40 | { 41 | var (workspaceDirectory, _) = Helpers.CreateEnvironment(6); 42 | RunServer(workspaceDirectory); 43 | RunClient(Directory.GetCurrentDirectory()); 44 | _server.Stop(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Src/Examples/Example4/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Example4")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Example4")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("85ec4f92-a7cd-49f2-ab2d-57b9cbd1adbf")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Src/ProgramUpdaterSln.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F1C186E9-2666-448C-B634-02D208C8C68A}" 7 | ProjectSection(SolutionItems) = preProject 8 | build.fsx = build.fsx 9 | paket.dependencies = paket.dependencies 10 | EndProjectSection 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B20D108B-7063-4FC3-8227-4085E163DA78}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{C0083A5C-9720-4D88-B766-D466A6D33B2A}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example3", "Examples\Example3\Example3.csproj", "{3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example4", "Examples\Example4\Example4.csproj", "{85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Lib", "Lib", "{2231F5C6-089D-4C3A-AEB0-B9FDDA092EE4}" 21 | EndProject 22 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ES.Update", "ES.Update\ES.Update.fsproj", "{D08E89F2-7F4E-42DE-A775-95447764FC4A}" 23 | EndProject 24 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ES.Update.Backend", "ES.Update.Backend\ES.Update.Backend.fsproj", "{52FB72CD-AA4A-47F4-9B47-A8DDB2580426}" 25 | EndProject 26 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ES.Update.Releaser", "ES.Update.Releaser\ES.Update.Releaser.fsproj", "{1EA88AB4-F0F9-4568-B9D7-F929BE54282E}" 27 | EndProject 28 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "UnitTests", "Tests\UnitTests\UnitTests.fsproj", "{3D4E04B2-A735-413C-AE11-1607D9DDA51A}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example1", "Examples\Example1\Example1.csproj", "{FB2F41AA-8607-4197-9319-277CCEF553BF}" 31 | EndProject 32 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{298CDF37-C12D-4B00-B313-53E665F89BF4}" 33 | EndProject 34 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "KeysGenerator", "Tools\KeysGenerator\KeysGenerator.fsproj", "{C72BCAF0-9DF8-4CAC-A912-01355BB4DDE4}" 35 | EndProject 36 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "VersionReleaser", "Tools\VersionReleaser\VersionReleaser.fsproj", "{67BE6366-B644-49DF-BB9F-D9DEE52097C4}" 37 | EndProject 38 | Global 39 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 40 | Debug|Any CPU = Debug|Any CPU 41 | Release|Any CPU = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 44 | {3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {D08E89F2-7F4E-42DE-A775-95447764FC4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {D08E89F2-7F4E-42DE-A775-95447764FC4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {D08E89F2-7F4E-42DE-A775-95447764FC4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {D08E89F2-7F4E-42DE-A775-95447764FC4A}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {52FB72CD-AA4A-47F4-9B47-A8DDB2580426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {52FB72CD-AA4A-47F4-9B47-A8DDB2580426}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {52FB72CD-AA4A-47F4-9B47-A8DDB2580426}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {52FB72CD-AA4A-47F4-9B47-A8DDB2580426}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {1EA88AB4-F0F9-4568-B9D7-F929BE54282E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {1EA88AB4-F0F9-4568-B9D7-F929BE54282E}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {1EA88AB4-F0F9-4568-B9D7-F929BE54282E}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {1EA88AB4-F0F9-4568-B9D7-F929BE54282E}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {3D4E04B2-A735-413C-AE11-1607D9DDA51A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {3D4E04B2-A735-413C-AE11-1607D9DDA51A}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {3D4E04B2-A735-413C-AE11-1607D9DDA51A}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {3D4E04B2-A735-413C-AE11-1607D9DDA51A}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {FB2F41AA-8607-4197-9319-277CCEF553BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {FB2F41AA-8607-4197-9319-277CCEF553BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {FB2F41AA-8607-4197-9319-277CCEF553BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {FB2F41AA-8607-4197-9319-277CCEF553BF}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {C72BCAF0-9DF8-4CAC-A912-01355BB4DDE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {C72BCAF0-9DF8-4CAC-A912-01355BB4DDE4}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {C72BCAF0-9DF8-4CAC-A912-01355BB4DDE4}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {C72BCAF0-9DF8-4CAC-A912-01355BB4DDE4}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {67BE6366-B644-49DF-BB9F-D9DEE52097C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {67BE6366-B644-49DF-BB9F-D9DEE52097C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {67BE6366-B644-49DF-BB9F-D9DEE52097C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {67BE6366-B644-49DF-BB9F-D9DEE52097C4}.Release|Any CPU.Build.0 = Release|Any CPU 80 | EndGlobalSection 81 | GlobalSection(SolutionProperties) = preSolution 82 | HideSolutionNode = FALSE 83 | EndGlobalSection 84 | GlobalSection(NestedProjects) = preSolution 85 | {3CB4C2C6-2E3D-4DDA-95BF-67CB19DC0F50} = {C0083A5C-9720-4D88-B766-D466A6D33B2A} 86 | {85EC4F92-A7CD-49F2-AB2D-57B9CBD1ADBF} = {C0083A5C-9720-4D88-B766-D466A6D33B2A} 87 | {D08E89F2-7F4E-42DE-A775-95447764FC4A} = {2231F5C6-089D-4C3A-AEB0-B9FDDA092EE4} 88 | {52FB72CD-AA4A-47F4-9B47-A8DDB2580426} = {2231F5C6-089D-4C3A-AEB0-B9FDDA092EE4} 89 | {1EA88AB4-F0F9-4568-B9D7-F929BE54282E} = {2231F5C6-089D-4C3A-AEB0-B9FDDA092EE4} 90 | {3D4E04B2-A735-413C-AE11-1607D9DDA51A} = {B20D108B-7063-4FC3-8227-4085E163DA78} 91 | {FB2F41AA-8607-4197-9319-277CCEF553BF} = {C0083A5C-9720-4D88-B766-D466A6D33B2A} 92 | {C72BCAF0-9DF8-4CAC-A912-01355BB4DDE4} = {298CDF37-C12D-4B00-B313-53E665F89BF4} 93 | {67BE6366-B644-49DF-BB9F-D9DEE52097C4} = {298CDF37-C12D-4B00-B313-53E665F89BF4} 94 | EndGlobalSection 95 | GlobalSection(ExtensibilityGlobals) = postSolution 96 | SolutionGuid = {A497458A-92F6-47AB-9C90-3103B990C6F6} 97 | EndGlobalSection 98 | EndGlobal 99 | -------------------------------------------------------------------------------- /Src/Tests/UnitTests/BackendTests.fs: -------------------------------------------------------------------------------- 1 | namespace UnitTests 2 | 3 | open System 4 | open System.Reflection 5 | open System.Text 6 | open ES.Update 7 | 8 | module BackendTests = 9 | let private ``Encrypt and Decrypt exported key``() = 10 | let password = "testPassword" 11 | let privateKey = 12 | "This private key value will be used to test the export and import feature" 13 | |> Encoding.UTF8.GetBytes 14 | |> Convert.ToBase64String 15 | 16 | // export key 17 | let exportedKey = encryptExportedKey(password, privateKey) 18 | 19 | // import key 20 | let importedKey = decryptImportedKey(password, exportedKey) 21 | 22 | // verify 23 | let testResult = importedKey.Equals(privateKey, StringComparison.Ordinal) 24 | assert testResult 25 | 26 | let runAll() = 27 | ``Encrypt and Decrypt exported key``() 28 | -------------------------------------------------------------------------------- /Src/Tests/UnitTests/CryptoUtilityTests.fs: -------------------------------------------------------------------------------- 1 | namespace UnitTests 2 | 3 | open System 4 | open System.Reflection 5 | open System.Text 6 | open ES.Update 7 | open System.Net.NetworkInformation 8 | open System.IO 9 | 10 | module CryptoUtilityTests = 11 | let private ``Sign data and verify``() = 12 | // generate signature 13 | let data = Encoding.UTF8.GetBytes("This buffer contains data that must be signed") 14 | let (publicKey, privateKey) = CryptoUtility.generateKeys() 15 | let signature = CryptoUtility.sign(data, privateKey) 16 | 17 | // verify signature 18 | let testResult = CryptoUtility.verifyData(data, signature, publicKey) 19 | assert testResult 20 | 21 | let private ``Sign data and verify a corrupted data``() = 22 | // generate signature 23 | let data = Encoding.UTF8.GetBytes("This buffer contains data that must be signed") 24 | let (publicKey, privateKey) = CryptoUtility.generateKeys() 25 | let signature = CryptoUtility.sign(data, privateKey) 26 | 27 | // verify signature 28 | data.[2] <- data.[2] ^^^ 0xA1uy 29 | let testResult = CryptoUtility.verifyData(data, signature, publicKey) 30 | assert(not testResult) 31 | 32 | let private ``Sign data and verify a corrupted signature``() = 33 | // generate signature 34 | let data = Encoding.UTF8.GetBytes("This buffer contains data that must be signed") 35 | let (publicKey, privateKey) = CryptoUtility.generateKeys() 36 | let signature = CryptoUtility.sign(data, privateKey) 37 | 38 | // verify signature 39 | signature.[2] <- signature.[2] ^^^ 0xA1uy 40 | let testResult = CryptoUtility.verifyData(data, signature, publicKey) 41 | assert(not testResult) 42 | 43 | let private ``Encrypt and decrypt data``() = 44 | let text = "This is the text that must be used in order to verify if the encryption and decryption work correctly" 45 | let key = getMacAddresses() |> CryptoUtility.sha256Raw 46 | let iv = getHardDiskSerials() 47 | 48 | // encrypt and decrypt 49 | let textBytes = Encoding.UTF8.GetBytes(text) 50 | let encryptedText = CryptoUtility.encrypt(textBytes, key, iv) 51 | let decryptedText = CryptoUtility.decrypt(encryptedText, key, iv) |> Encoding.UTF8.GetString 52 | 53 | // check 54 | let testResult = decryptedText.Equals(text, StringComparison.Ordinal) 55 | assert testResult 56 | 57 | let runAll() = 58 | ``Sign data and verify``() 59 | ``Sign data and verify a corrupted data``() 60 | ``Sign data and verify a corrupted signature``() 61 | ``Encrypt and decrypt data``() 62 | -------------------------------------------------------------------------------- /Src/Tests/UnitTests/Program.fs: -------------------------------------------------------------------------------- 1 | namespace UnitTests 2 | 3 | module Program = 4 | 5 | [] 6 | let main argv = 7 | CryptoUtilityTests.runAll() 8 | BackendTests.runAll() 9 | 0 10 | -------------------------------------------------------------------------------- /Src/Tests/UnitTests/UnitTests.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Src/Tools/KeysGenerator/KeysGenerator.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Src/Tools/KeysGenerator/Program.fs: -------------------------------------------------------------------------------- 1 | namespace Installer 2 | 3 | open System 4 | open Argu 5 | open ES.Update 6 | open System.Diagnostics 7 | open System.Threading 8 | open System.Text 9 | open System.Text.RegularExpressions 10 | open ES.Fslog 11 | 12 | module Program = 13 | open System.IO 14 | 15 | type CLIArguments = 16 | | Public of path:String 17 | | Private of path:String 18 | with 19 | interface IArgParserTemplate with 20 | member s.Usage = 21 | match s with 22 | | Public _ -> "if specified, the file where to save the public key." 23 | | Private _ -> "if specified, the file where to save the private key." 24 | 25 | let printColor(msg: String, color: ConsoleColor) = 26 | Console.ForegroundColor <- color 27 | Console.WriteLine(msg) 28 | Console.ResetColor() 29 | 30 | let printError(errorMsg: String) = 31 | printColor(errorMsg, ConsoleColor.Red) 32 | 33 | let printBanner() = 34 | Console.ForegroundColor <- ConsoleColor.Cyan 35 | let banner = "-=[ Keys Generator ]=-" 36 | let year = if DateTime.Now.Year = 2019 then "2019" else String.Format("2019-{0}", DateTime.Now.Year) 37 | let copy = String.Format("Copyright (c) {0} Enkomio {1}", year, Environment.NewLine) 38 | Console.WriteLine(banner) 39 | Console.WriteLine(copy) 40 | Console.ResetColor() 41 | 42 | let printUsage(body: String) = 43 | Console.WriteLine(body) 44 | 45 | [] 46 | let main argv = 47 | printBanner() 48 | 49 | let parser = ArgumentParser.Create() 50 | try 51 | let results = parser.Parse(argv) 52 | 53 | if results.IsUsageRequested then 54 | printUsage(parser.PrintUsage()) 55 | 0 56 | else 57 | let (publicKeyBytes, privateKeyBytes) = CryptoUtility.generateKeys() 58 | let (publicKey, privateKey) = (publicKeyBytes |> Convert.ToBase64String, privateKeyBytes |> Convert.ToBase64String) 59 | 60 | Console.WriteLine("Public key: " + publicKey) 61 | Console.WriteLine() 62 | Console.WriteLine("Private key: " + privateKey) 63 | Console.WriteLine() 64 | 65 | results.TryGetResult(<@ Public @>) 66 | |> Option.iter(fun file -> 67 | File.WriteAllText(file, publicKey) 68 | Console.WriteLine("Public key saved to: " + file) 69 | ) 70 | 71 | results.TryGetResult(<@ Public @>) 72 | |> Option.iter(fun file -> 73 | File.WriteAllText(file, privateKey) 74 | Console.WriteLine("Private key saved to: " + file) 75 | ) 76 | 0 77 | with 78 | | :? ArguParseException -> 79 | printUsage(parser.PrintUsage()) 80 | 1 81 | | e -> 82 | printError(e.ToString()) 83 | 1 84 | -------------------------------------------------------------------------------- /Src/Tools/KeysGenerator/paket.references: -------------------------------------------------------------------------------- 1 | Argu 2 | ES.Fslog.Core -------------------------------------------------------------------------------- /Src/Tools/VersionReleaser/Program.fs: -------------------------------------------------------------------------------- 1 | namespace VersionReleaser 2 | 3 | open System 4 | open System.IO 5 | open System.Reflection 6 | open Argu 7 | open ES.Fslog 8 | open ES.Fslog.Loggers 9 | open ES.Fslog.TextFormatters 10 | open ES.Update.Releaser 11 | 12 | module Program = 13 | type CLIArguments = 14 | | [] File of file:String 15 | | Working_Dir of path:String 16 | with 17 | interface IArgParserTemplate with 18 | member s.Usage = 19 | match s with 20 | | File _ -> "the release file to analyze in order to generate the metadata." 21 | | Working_Dir _ -> "the directory where the update artifacts will be saved." 22 | 23 | let printColor(msg: String, color: ConsoleColor) = 24 | Console.ForegroundColor <- color 25 | Console.WriteLine(msg) 26 | Console.ResetColor() 27 | 28 | let printError(errorMsg: String) = 29 | printColor(errorMsg, ConsoleColor.Red) 30 | 31 | let printBanner() = 32 | Console.ForegroundColor <- ConsoleColor.Cyan 33 | let banner = "-=[ Version Releaser ]=-" 34 | let year = if DateTime.Now.Year = 2019 then "2019" else String.Format("2019-{0}", DateTime.Now.Year) 35 | let copy = String.Format("Copyright (c) {0} Enkomio {1}", year, Environment.NewLine) 36 | Console.WriteLine(banner) 37 | Console.WriteLine(copy) 38 | Console.ResetColor() 39 | 40 | let createLogProvider() = 41 | let logProvider = new LogProvider() 42 | let logFile = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "version-releaser.log") 43 | logProvider.AddLogger(new ConsoleLogger(LogLevel.Informational, new ConsoleLogFormatter())) 44 | logProvider.AddLogger(new FileLogger(LogLevel.Informational, logFile)) 45 | logProvider 46 | 47 | let printUsage(body: String) = 48 | Console.WriteLine(body) 49 | 50 | [] 51 | let main argv = 52 | printBanner() 53 | 54 | let parser = ArgumentParser.Create() 55 | try 56 | let results = parser.Parse(argv) 57 | 58 | if results.IsUsageRequested then 59 | printUsage(parser.PrintUsage()) 60 | 0 61 | else 62 | let workingDir = results.GetResult(<@ Working_Dir @>, Settings.Read().WorkspaceDirectory) 63 | Directory.CreateDirectory(workingDir) |> ignore 64 | let filename = results.GetResult(<@ File @>) 65 | let metadataBuilder = new MetadataBuilder(workingDir, Settings.Read().PatternToExclude, createLogProvider()) 66 | metadataBuilder.CreateReleaseMetadata(filename) 67 | 0 68 | with 69 | | :? ArguParseException -> 70 | printUsage(parser.PrintUsage()) 71 | 1 72 | | e -> 73 | printError(e.ToString()) 74 | 1 75 | -------------------------------------------------------------------------------- /Src/Tools/VersionReleaser/Settings.fs: -------------------------------------------------------------------------------- 1 | namespace VersionReleaser 2 | 3 | open System 4 | open System.Collections.Generic 5 | open System.IO 6 | open Newtonsoft.Json 7 | open System.Reflection 8 | 9 | type Settings() = 10 | member val WorkspaceDirectory = String.Empty with get, set 11 | member val PatternToExclude = new List() with get, set 12 | 13 | static member Read() = 14 | let configFile = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "configuration.json") 15 | if File.Exists(configFile) then 16 | (File.ReadAllText >> JsonConvert.DeserializeObject)(configFile) 17 | else 18 | new Settings() -------------------------------------------------------------------------------- /Src/Tools/VersionReleaser/VersionReleaser.fsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | Always 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Src/Tools/VersionReleaser/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkspaceDirectory": "updates", 3 | "PatternToExclude": [ 4 | "\\.lic$", 5 | "\\.log$" 6 | ] 7 | } -------------------------------------------------------------------------------- /Src/Tools/VersionReleaser/paket.references: -------------------------------------------------------------------------------- 1 | ES.Fslog.Core 2 | Argu 3 | Newtonsoft.Json -------------------------------------------------------------------------------- /Src/build.fsx: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------- 2 | // FAKE build script 3 | // -------------------------------------------------------------------------------------- 4 | #r "paket: groupref FakeBuild //" 5 | #load ".fake/build.fsx/intellisense.fsx" 6 | 7 | #r @"System.IO.Compression" 8 | #r @"System.IO.Compression.FileSystem" 9 | 10 | open System 11 | open System.Diagnostics 12 | open System.IO 13 | open Fake 14 | open Fake.Core.TargetOperators 15 | open Fake.DotNet 16 | open Fake.Core 17 | open Fake.IO 18 | 19 | // the project file name 20 | let projectFileName = "ProgramUpdater" 21 | 22 | // The name of the project 23 | let project = "Program Updater Framework" 24 | 25 | // Short summary of the project 26 | let summary = "A framework to automatize the process of updating a program in an efficent and secure way." 27 | 28 | // List of author names 29 | let authors = "Enkomio" 30 | 31 | // Build dir 32 | let buildDir = "build" 33 | 34 | // Release dir 35 | let releaseDir = "release" 36 | 37 | // Extension to not include in release 38 | let forbiddenExtensions = [".pdb"] 39 | 40 | // Projecy Guid 41 | let projectGuid = "0F026EA5-501A-4947-B8E2-5860D3520E99" 42 | 43 | // F# project names 44 | let fsharpProjects = [ 45 | "ES.Update" 46 | "ES.Update.Backend" 47 | "ES.Update.Releaser" 48 | "Installer" 49 | "Updater" 50 | "UpdateServer" 51 | "VersionReleaser" 52 | ] 53 | 54 | // C# project names 55 | let csharpProjects = [ 56 | ] 57 | 58 | ////////////////////////////////////////////////////////// 59 | // All code below should be generic enought // 60 | // to not be modified in order to build the solution // 61 | ////////////////////////////////////////////////////////// 62 | 63 | // set the script dir as current 64 | Directory.SetCurrentDirectory(__SOURCE_DIRECTORY__) 65 | 66 | // Read additional information from the release notes document 67 | let releaseNotesData = 68 | let changelogFile = Path.Combine("..", "RELEASE_NOTES.md") 69 | File.ReadAllLines(changelogFile) 70 | |> ReleaseNotes.parse 71 | 72 | let releaseNoteVersion = Version.Parse(releaseNotesData.AssemblyVersion) 73 | let now = DateTime.UtcNow 74 | let timeSpan = now.Subtract(new DateTime(1980,2,1,0,0,0)) 75 | let months = timeSpan.TotalDays / 30. |> int32 76 | let remaining = int32 timeSpan.TotalDays - months * 30 77 | let releaseVersion = string <| new Version(releaseNoteVersion.Major, releaseNoteVersion.Minor, months, remaining) 78 | Trace.trace("Build Version: " + releaseVersion) 79 | 80 | // Targets 81 | Core.Target.create "Clean" (fun _ -> 82 | Fake.IO.Shell.cleanDirs [buildDir; releaseDir] 83 | ) 84 | 85 | Core.Target.create "SetAssemblyInfo" (fun _ -> 86 | fsharpProjects 87 | |> List.iter(fun projName -> 88 | let fileName = Path.Combine(projName, "AssemblyInfo.fs") 89 | AssemblyInfoFile.createFSharp fileName [ 90 | DotNet.AssemblyInfo.Title project 91 | DotNet.AssemblyInfo.Product project 92 | DotNet.AssemblyInfo.Guid projectGuid 93 | DotNet.AssemblyInfo.Company authors 94 | DotNet.AssemblyInfo.Description summary 95 | DotNet.AssemblyInfo.Version releaseVersion 96 | DotNet.AssemblyInfo.FileVersion releaseVersion 97 | DotNet.AssemblyInfo.InformationalVersion releaseVersion 98 | DotNet.AssemblyInfo.Metadata("BuildDate", DateTime.UtcNow.ToString("yyyy-MM-dd")) 99 | ] 100 | ) 101 | 102 | csharpProjects 103 | |> List.iter(fun projName -> 104 | let fileName = Path.Combine(projName, "AssemblyInfo.cs") 105 | AssemblyInfoFile.createCSharp fileName [ 106 | DotNet.AssemblyInfo.Title project 107 | DotNet.AssemblyInfo.Product project 108 | DotNet.AssemblyInfo.Guid projectGuid 109 | DotNet.AssemblyInfo.Company authors 110 | DotNet.AssemblyInfo.Description summary 111 | DotNet.AssemblyInfo.Version releaseVersion 112 | DotNet.AssemblyInfo.FileVersion releaseVersion 113 | DotNet.AssemblyInfo.InformationalVersion releaseVersion 114 | DotNet.AssemblyInfo.Metadata("BuildDate", DateTime.UtcNow.ToString("yyyy-MM-dd")) 115 | ] 116 | ) 117 | ) 118 | 119 | Core.Target.create "Compile" (fun _ -> 120 | fsharpProjects 121 | |> List.iter(fun projectName -> 122 | let project = Path.Combine(projectName, projectName + ".fsproj") 123 | let buildAppDir = Path.Combine(buildDir, projectName) 124 | Fake.IO.Directory.ensure buildAppDir 125 | 126 | // compile 127 | DotNet.MSBuild.runRelease id buildAppDir "Build" [project] 128 | |> Trace.logItems "Build Output: " 129 | ) 130 | 131 | csharpProjects 132 | |> List.iter(fun projectName -> 133 | let project = Path.Combine(projectName, projectName + ".csproj") 134 | let buildAppDir = Path.Combine(buildDir, projectName) 135 | Fake.IO.Directory.ensure buildAppDir 136 | 137 | // compile 138 | DotNet.MSBuild.runRelease id buildAppDir "Build" [project] 139 | |> Trace.logItems "Build Output: " 140 | ) 141 | ) 142 | 143 | Core.Target.create "CleanBuild" (fun _ -> 144 | Directory.GetFiles(buildDir, "*.*", SearchOption.AllDirectories) 145 | |> Array.filter(fun file -> 146 | forbiddenExtensions 147 | |> List.contains (Path.GetExtension(file).ToLowerInvariant()) 148 | ) 149 | |> Array.iter(File.Delete) 150 | ) 151 | 152 | Core.Target.create "MergeInstaller" (fun _ -> 153 | let ilMerge = Path.Combine("Src", "packages", "ilmerge", "tools", "net452", "ILMerge.exe") 154 | 155 | [("Installer", "Installer.exe")] 156 | |> List.iter(fun (projectName, mainExecutable) -> 157 | let projectDir = Path.Combine(buildDir, projectName) 158 | let mainExe = Path.Combine(projectDir, mainExecutable) 159 | let allFiles = Directory.GetFiles(projectDir, "*.dll", SearchOption.AllDirectories) |> List.ofArray 160 | let arguments = String.Join(" ", mainExe::allFiles) 161 | Process.Start(ilMerge, arguments).WaitForExit() 162 | ) 163 | ) 164 | 165 | Core.Target.create "Release" (fun _ -> 166 | let releaseDirectory = Path.Combine(releaseDir, String.Format("{0}.v{1}", projectFileName, releaseVersion)) 167 | Directory.CreateDirectory(releaseDirectory) |> ignore 168 | 169 | // copy all binaries in Bin directory 170 | fsharpProjects@csharpProjects 171 | |> List.iter(fun projName -> 172 | let buildProjectDir = Path.Combine(buildDir, projName) 173 | Shell.copyDir releaseDirectory buildProjectDir (fun _ -> true) 174 | ) 175 | 176 | // create zip file 177 | let releaseFilename = releaseDirectory + ".zip" 178 | Directory.GetFiles(releaseDirectory, "*.*", SearchOption.AllDirectories) 179 | |> Fake.IO.Zip.zip releaseDirectory releaseFilename 180 | ) 181 | 182 | "Clean" 183 | ==> "SetAssemblyInfo" 184 | ==> "Compile" 185 | ==> "CleanBuild" 186 | ==> "MergeInstaller" 187 | ==> "Release" 188 | 189 | // Start build 190 | Core.Target.runOrDefault "Release" -------------------------------------------------------------------------------- /Src/mergeInstaller.fsx: -------------------------------------------------------------------------------- 1 | open System 2 | open System.IO 3 | open System.Diagnostics 4 | 5 | let ilMerge = Path.Combine("packages", "ilmerge", "tools", "net452", "ILMerge.exe") 6 | 7 | [ 8 | (Path.Combine("Installer", "bin", "Release"), "Installer.exe") 9 | (Path.Combine("Installer.Core", "bin", "Release", "netcoreapp3.1"), "Installer.exe") 10 | ] 11 | |> List.iter(fun (projectDir, mainExecutable) -> 12 | let mainExe = Path.Combine(projectDir, mainExecutable) 13 | let allFiles = 14 | Directory.GetFiles(projectDir, "*.dll", SearchOption.AllDirectories) 15 | |> List.ofArray 16 | |> List.map(Path.GetFullPath) 17 | 18 | let arguments = String.Join(" ", mainExe::allFiles) 19 | Console.WriteLine("Cmd: {0} {1}", ilMerge, arguments) 20 | Process.Start(ilMerge, arguments).WaitForExit() 21 | ) -------------------------------------------------------------------------------- /Src/nuget.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enkomio/ProgramUpdater/fdec3aa7f71e96a1e0ac1468c39a27d291058e93/Src/nuget.exe -------------------------------------------------------------------------------- /Src/paket.dependencies: -------------------------------------------------------------------------------- 1 | version 5.206.0 2 | source https://nuget.org/api/v2 3 | 4 | nuget FSharp.Core 5 | nuget Suave 6 | nuget Newtonsoft.Json 7 | nuget Argu 8 | nuget BouncyCastle 9 | nuget ES.Fslog.Core 10 | nuget ilmerge ~> 3.0.29 11 | 12 | group FakeBuild 13 | source https://api.nuget.org/v3/index.json 14 | nuget Fake.Core.Target 15 | nuget Fake.IO.FileSystem 16 | nuget Fake.DotNet.MSBuild 17 | nuget Fake.Core.ReleaseNotes 18 | nuget Fake.DotNet.AssemblyInfoFile 19 | nuget Fake.Core.Trace 20 | nuget Fake.IO.Zip 21 | nuget Fake.Core.Process -------------------------------------------------------------------------------- /Src/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enkomio/ProgramUpdater/fdec3aa7f71e96a1e0ac1468c39a27d291058e93/Src/paket.exe -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cls 3 | 4 | @rem Move to src directory 5 | cd Src 6 | 7 | @rem install fake 8 | dotnet tool install fake-cli --tool-path fake 9 | 10 | @rem paket.exe install 11 | paket.exe install 12 | if errorlevel 1 ( 13 | exit /b %errorlevel% 14 | ) 15 | 16 | ".\fake\fake.exe" run build.fsx %* 17 | 18 | cd .. --------------------------------------------------------------------------------