├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── config.go ├── file.go └── operation.go ├── connectiontest ├── connectiontest └── connectiontest.go ├── go.mod ├── go.sum ├── server ├── auth.go ├── auth_test.go ├── errors.go ├── server.go ├── server_test.go ├── static │ ├── gateway.css │ ├── tooltip.svg │ └── welcome.png ├── template.go ├── templates │ ├── base.html │ ├── editAccount.html │ ├── editService.html │ ├── key.html │ ├── list.html │ ├── upload.html │ ├── user.html │ └── welcome.png └── ui.go ├── setuptool └── setuptool.go └── testserver └── testserver.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | To send patches and contribute, there are just a few small guidelines you need 4 | to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use Gerrit pull requests for code review. 22 | 23 | ## Community Guidelines 24 | 25 | This project follows [Google's Open Source Community 26 | Guidelines](https://opensource.google.com/conduct/). 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | ENV GO111MODULE=on 4 | 5 | ADD . /go/src/github.com/google/web-api-gateway 6 | 7 | RUN go install github.com/google/web-api-gateway/server@latest 8 | RUN go install github.com/google/web-api-gateway/setuptool@latest 9 | RUN go install github.com/google/web-api-gateway/connectiontest@latest 10 | 11 | ENTRYPOINT ["/go/bin/server"] 12 | 13 | EXPOSE 443 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web API Gateway 2 | 3 | Web API Gateway allows you to selectivley share API access with partners. 4 | 5 | # Setup Guide 6 | 7 | ## 1. Acquire Account Access 8 | 9 | To access the API for a specific account, you will need the ability to authorize 10 | web-api-gateway's access. This typically means being able to log into this 11 | account. You likely already have this for accounts you wish to use. 12 | 13 | ## 2. Acquire API Access 14 | 15 | You will need OAuth2 access to the api you wish to connect to. If you have 16 | access, you will have a “Client ID”, and a “Client Secret”. Where these are 17 | located will vary by website, but can be accessed with the account that was 18 | given access to the API. 19 | 20 | ## 3. Server 21 | 22 | You will need somewhere to run Web API Gateway. Your options include one of the 23 | following: 24 | 25 | * A virtual machine running on a cloud provider. Recommended, this is what 26 | this guide will follow. 27 | * Kubernetes. If you have an existing Kubernetes cluster, you may leverage 28 | that to run your service. 29 | * An on premise machine in your own data center. 30 | 31 | Your choice must be capable of: 32 | 33 | * Running a docker container. 34 | * Providing persistent storage for that container. 35 | * Exposing https ports to the internet. 36 | 37 | ### Starting your VM. 38 | 39 | On your cloud provider of choice, create a new VM instance. 40 | 41 | We recommend: 42 | 43 | * Choosing a low powered VM. No need to go overboard on size and capabilities. 44 | This is a fairly simple, low load server. If anything becomes a bottleneck, 45 | it is likely the network, so low ram and processing power are fine. You can 46 | upgrade later if that’s a problem. 47 | * The latest version of Ubuntu, though your favorite variant of linux will 48 | likely be fine. 49 | 50 | ## 4. Expose to external internet 51 | 52 | If you’re not using a cloud based VM, this will depend heavily on your local 53 | setup. 54 | 55 | In your cloud provider, your VM must have a reserved static external IP address. 56 | This may either be an option when creating/editing your VM, or it may be a 57 | separate option after creation. 58 | 59 | You also must set any firewall settings to expose port 443. If you are using 60 | Let’s Encrypt to provide a certificate for https, you must also expose port 80. 61 | 62 | ## 5. Domain Name 63 | 64 | You probably have an existing domain name which you can use a subdomain from. 65 | Use your favorite domain name registered, and add an “A” record which points to 66 | the domain name of your choice to the external IP address from the previous 67 | step. For the remaining examples, replace “web-api-gateway.example.com” with 68 | your actual domain. 69 | 70 | Once you have a domain name, you will need to add it to the list of allowed 71 | redirect domains on the API you want to use. This is likely on a management page 72 | related to where you can find the "Client ID" and "Client Secret" for your API 73 | access. 74 | 75 | ## 6. SSL Certificate 76 | 77 | You must set up certificates for your Web API Gateway. This allows clients to 78 | connect to Web API Gateway with HTTPS, and know they’re talking to the correct 79 | server. If you have an existing process for creating and managing certificates, 80 | you will likely want to use that. 81 | 82 | The easiest way to quickly get a certificate for the domain used for Web API 83 | Gateway is by using Let’s Encrypt. 84 | 85 | 1. Go to https://certbot.eff.org/ 86 | 2. Select “None of the above” for your choice of Software 87 | 3. Select the system you’re using. 88 | 4. You’ll want to use the “--standalone” method with the additional flag 89 | “--preferred-challenges http”. This will use the standard http port, and 90 | won’t conflict with web-api-gateway. You can ignore the instructions to use 91 | “--webroot“, as web-api-gateway doesn’t serve any local files. 92 | 93 | Our command looks like this: (*replace the example url with your url*) 94 | 95 | ``` 96 | sudo certbot certonly --standalone --preferred-challenges http -d web-api-gateway.example.com 97 | ``` 98 | 99 | This should give you feedback like the following: 100 | 101 | ``` 102 | sudo certbot certonly --standalone --preferred-challenges http -d web-api-gateway.example.com 103 | Saving debug log to /var/log/letsencrypt/letsencrypt.log 104 | Plugins selected: Authenticator standalone, Installer None 105 | Obtaining a new certificate 106 | Performing the following challenges: 107 | http-01 challenge for web-api-gateway.example.com 108 | Waiting for verification... 109 | Cleaning up challenges 110 | 111 | IMPORTANT NOTES: 112 | - Congratulations! Your certificate and chain have been saved at: 113 | /etc/letsencrypt/live/web-api-gateway.example.com/fullchain.pem 114 | Your key file has been saved at: 115 | /etc/letsencrypt/live/web-api-gateway.example.com/privkey.pem 116 | Your cert will expire on 2018-12-18. To obtain a new or tweaked 117 | version of this certificate in the future, simply run certbot 118 | again. To non-interactively renew *all* of your certificates, run 119 | "certbot renew" 120 | - If you like Certbot, please consider supporting our work by: 121 | 122 | Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate 123 | Donating to EFF: https://eff.org/donate-le 124 | ``` 125 | 126 | If you encountered a problem related to “binding to port 80”, your vm may 127 | already have bound a web server to port 80. If it’s apache, you can stop it 128 | with: 129 | 130 | ``` 131 | sudo /etc/init.d/apache2 stop 132 | ``` 133 | 134 | 5. Verify that your certs are present by running: (*replace the example url 135 | with your url*) 136 | 137 | ``` 138 | ls /etc/letsencrypt/live/web-api-gateway.example.com/ 139 | ``` 140 | 141 | 6. Let’s Encrypt certificates only last for 90 days. However certbot should 142 | have set up a cron job to automatically renew it. 143 | 144 | ## 7. Getting Web API Gateway 145 | 146 | The code is located at: https://github.com/google/web-api-gateway 147 | 148 | ### Install Prerequisites: 149 | 150 | #### Git: 151 | 152 | Git is a code storing program, which will be able to retrieve a current copy of 153 | Web API Gateway’s source code. If you’re using the suggested Ubuntu VM approach, 154 | run this command to install Git: 155 | 156 | ``` 157 | sudo apt install git-all 158 | ``` 159 | 160 | #### Curl: 161 | 162 | Curl retrieves web pages, is just used to install Docker: 163 | 164 | ``` 165 | sudo apt install curl 166 | ``` 167 | 168 | #### Docker: 169 | 170 | Dockers handles building and running Web API Gateway. If you’re using the 171 | suggested Ubuntu VM approach, run these commands to install Docker: 172 | 173 | ``` 174 | curl -fsSL get.docker.com -o get-docker.sh 175 | sh get-docker.sh 176 | ``` 177 | 178 | ### Clone the code: 179 | 180 | Run this command to retrieve the latest version of the code. 181 | 182 | ``` 183 | cd ~ 184 | git clone https://github.com/google/web-api-gateway.git 185 | ``` 186 | 187 | Only If you already have the code copied, and are updating to a new version, 188 | instead run: 189 | 190 | ``` 191 | cd ~/web-api-gateway 192 | git pull 193 | 194 | ``` 195 | 196 | Create a docker image for running in docker: 197 | 198 | ``` 199 | cd ~/web-api-gateway 200 | sudo docker build -t web-api-gateway . 201 | ``` 202 | 203 | ## 8. Running Web API Gateway 204 | 205 | Now you’re all set to start the server: (*replace the example url with your 206 | url*) 207 | 208 | ``` 209 | cd ~/web-api-gateway 210 | sudo docker run \ 211 | --publish 443:443 \ 212 | --name web-api-gateway \ 213 | -d \ 214 | -it \ 215 | --restart unless-stopped \ 216 | --volume /etc/letsencrypt/live/web-api-gateway.example.com/:/etc/letsencrypt/live/web-api-gateway.example.com/ \ 217 | --volume /etc/letsencrypt/archive/web-api-gateway.example.com/:/etc/letsencrypt/archive/web-api-gateway.example.com/ \ 218 | --volume /etc/webapigateway/config/:/etc/webapigateway/config/ \ 219 | web-api-gateway \ 220 | --certFile=/etc/letsencrypt/live/web-api-gateway.example.com/fullchain.pem \ 221 | --keyFile=/etc/letsencrypt/live/web-api-gateway.example.com/privkey.pem 222 | ``` 223 | 224 | Verify that the container is running by: 225 | 226 | ``` 227 | sudo docker container ls 228 | ``` 229 | 230 | If the container did not start successfully, remove `-d` (which causes the 231 | docker container to detatch from your shell) from the above command. This will 232 | show the output of the container as it is trying to start, including any error 233 | notices. This can help you determine what is causing the startup problems. 234 | 235 | ### If you're not using Docker 236 | 237 | Web API Gateway requires access to three files: 238 | 239 | * fullchain.pem 240 | * Used in ssl/https 241 | * By default looks for the file at `/etc/webapigateway/cert/fullchain.pem` 242 | * The path can be changed using the flag `certFile` 243 | * privekey.pem 244 | * Used in ssl/https 245 | * By default looks for the file at `/etc/webapigateway/cert/privkey.pem` 246 | * The path can be changed using the flag `keyFile` 247 | * The config. 248 | * Used to store the accounts and credentials for access. 249 | * If the Web API Gateway is started and this file does not exist, the 250 | server will start but only return errors. If you start the setuptool, it 251 | will create a new config. If you're running with Docker (or Kubernetes), 252 | it's recommended that you let Docker build everything, run the setuptool 253 | in the container, then restart the container. 254 | * By default looks for the file at `/etc/webapigateway/config/config.json` 255 | * The path can be changed using the flag `configpath` (this works with the 256 | setuptool too.) 257 | 258 | ## 9. Setting up your configuration 259 | 260 | In order to complete these steps, you will need the following: 261 | 262 | * Values provided by your API access (from step 2): 263 | * Client Id 264 | * Client Secret 265 | * Values that are dependant on the connection you're creating. These are 266 | typically provided by remote client which is accessing web-api-gateway. 267 | * Auth Url 268 | * Token Url 269 | * Scopes 270 | * Service Url 271 | 272 | This will start the interactive command line setup tool. 273 | 274 | ``` 275 | sudo docker exec --interactive web-api-gateway /go/bin/setuptool 276 | ``` 277 | 278 | When first started, you will be prompted for the domain name you chose. For our 279 | example, this is https://web-api-gateway.example.com. From there, you will be at 280 | the main menu. 281 | 282 | First, choose Add Service (option 3). This will prompt you for several values. 283 | For “client id” and “client secret”, you will want to use the values obtained in 284 | step 2. These are your personal API tokens. For the others, refer to the start 285 | of this step. 286 | 287 | After creating a service, choose “Add new account” (option 3). The service URL 288 | also should use a value from the start of this step. It is required you log into 289 | the account you wish to manage (step 1), and enter the authentication token that 290 | you are given. From the service level view (where you can create accounts.) use 291 | “back” (option 0), to go back to the main menu. From here, using “back” again 292 | will save and exit. 293 | 294 | web-api-gateway does not automatically load new configurations, so you must 295 | restart the server. 296 | 297 | ``` 298 | sudo docker restart web-api-gateway 299 | ``` 300 | 301 | ## 10. Retrieve Account Key to connect a client 302 | 303 | When you are asked for an account key to connect an application to the Web API 304 | Gateway, you can retrieve it using the same command as used to setup: 305 | 306 | ``` 307 | sudo docker exec --interactive web-api-gateway /go/bin/setuptool 308 | ``` 309 | 310 | At the main menu, run “Retrieve Account Key”, choose the account you want to 311 | link, and copy the Account Key that is provided. 312 | 313 | Use this to link a remote client to the web-api-gateway. You will be prompted 314 | for the account key from the remote service that you’re using. 315 | 316 | ## 11. Update Web API Gateway 317 | 318 | To update the Web API Gateway to a newer version: 319 | 320 | Pull the latest version of web-api-gateway: 321 | 322 | ``` 323 | cd ~/web-api-gateway 324 | sudo git pull 325 | ``` 326 | 327 | Stop and remove the old container: 328 | ``` 329 | sudo docker stop web-api-gateway 330 | sudo docker rm web-api-gateway 331 | ``` 332 | 333 | Rebuild the docker image with the new change: 334 | 335 | ``` 336 | sudo docker build -t web-api-gateway . 337 | ``` 338 | 339 | Start the server using [Step 8](#runservice) 340 | 341 | Verfiy the Web API Gateway is now on a newer version by visiting "/version" page. 342 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package config provides the config details for the services. 18 | package config 19 | 20 | import ( 21 | "golang.org/x/oauth2" 22 | ) 23 | 24 | // Config is the root for configuration of the web-api-gateway. 25 | type Config struct { 26 | Url string 27 | Users map[string]bool 28 | Services []*Service 29 | Template Template 30 | } 31 | 32 | // Service represents a distinct endpoint that can contain multiple account. 33 | type Service struct { 34 | ServiceName string // name for reference when setting up an account 35 | OauthServiceCreds *OauthServiceCreds 36 | Accounts []*Account 37 | EngineName string 38 | } 39 | 40 | // OauthServiceCreds stores the information to get authorized to connect new 41 | // accounts. 42 | type OauthServiceCreds struct { 43 | ClientID string // public identifier 44 | ClientSecret string // secret identifier 45 | AuthURL string // authenticaiton endpoint 46 | TokenURL string // token request endpoint 47 | Scopes []string // scopes of the requests 48 | } 49 | 50 | // Account stores the account name and the credential details for connections 51 | // to/from the web-api-gateway. 52 | type Account struct { 53 | AccountName string // name for this account 54 | ServiceURL string // service endpoint to be connected 55 | OauthAccountCreds *oauth2.Token 56 | ClientCreds *ClientCreds 57 | } 58 | 59 | // ClientCreds stores the autorization details to connect to this account. 60 | type ClientCreds struct { 61 | Protocol string // rule for encrypting messages 62 | PrivateKey string // key for encrypting messages 63 | } 64 | 65 | type Template struct { 66 | Engines []*Engine 67 | } 68 | 69 | type Engine struct { 70 | EngineName string 71 | AuthURL string 72 | TokenURL string 73 | Scopes string 74 | Domains []*Domain 75 | } 76 | 77 | type Domain struct { 78 | DomainName string 79 | ServiceURL string 80 | } 81 | -------------------------------------------------------------------------------- /config/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "encoding/json" 21 | "flag" 22 | "fmt" 23 | "io/ioutil" 24 | "os" 25 | ) 26 | 27 | var configFileName *string = flag.String( 28 | "configpath", 29 | "/etc/webapigateway/config/config.json", 30 | "This is the path to the json config file managing this proxy.", 31 | ) 32 | 33 | func ReadConfig() (*Config, error) { 34 | return readConfig(*configFileName) 35 | } 36 | 37 | func ReadWriteConfig() (c *Config, save func() error, err error) { 38 | return readWriteConfig(*configFileName) 39 | } 40 | 41 | func ReadTemplate() (*Template, error) { 42 | c, err := readConfig(*configFileName) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &c.Template, nil 47 | } 48 | 49 | func readConfig(name string) (*Config, error) { 50 | b, err := ioutil.ReadFile(name) 51 | if err != nil { 52 | return nil, fmt.Errorf("error reading file %s: %v", name, err) 53 | } 54 | 55 | c := Config{} 56 | err = json.Unmarshal(b, &c) 57 | if err != nil { 58 | return nil, fmt.Errorf("error parsing json in file %s: %v", name, err) 59 | } 60 | return &c, nil 61 | } 62 | 63 | func readWriteConfig(name string) (c *Config, save func() error, err error) { 64 | f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_SYNC, 0600) 65 | if err != nil { 66 | return nil, nil, fmt.Errorf("error reading or creating file %s: %v", name, err) 67 | } 68 | 69 | c = &Config{} 70 | { 71 | b, err := ioutil.ReadAll(f) 72 | if err != nil { 73 | return nil, nil, fmt.Errorf("error reading file %s: %v", name, err) 74 | } 75 | 76 | if len(b) > 0 { 77 | err = json.Unmarshal(b, &c) 78 | if err != nil { 79 | return nil, nil, fmt.Errorf("error parsing json in file %s: %v", name, err) 80 | } 81 | } 82 | } 83 | 84 | return c, saveConfig(name, c, f), nil 85 | } 86 | 87 | func saveConfig(name string, c *Config, f *os.File) func() error { 88 | return func() error { 89 | defer f.Close() 90 | 91 | b, err := json.MarshalIndent(&c, "", " ") 92 | if err != nil { 93 | return fmt.Errorf("Changes were not saved! Error making json: %v", err) 94 | } 95 | 96 | _, err = f.WriteAt(b, 0) 97 | if err != nil { 98 | return fmt.Errorf("Changes may not be not saved! Error writing to file %s: %v", name, err) 99 | } 100 | 101 | err = f.Truncate(int64(len(b))) 102 | if err != nil { 103 | return fmt.Errorf("Changes may not be not saved! Error writing to file %s: %v", name, err) 104 | } 105 | 106 | return nil 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /config/operation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "context" 21 | "crypto/ecdsa" 22 | "crypto/elliptic" 23 | "crypto/rand" 24 | "crypto/x509" 25 | "encoding/base64" 26 | "encoding/json" 27 | "errors" 28 | "fmt" 29 | "net/url" 30 | "regexp" 31 | "strings" 32 | 33 | "golang.org/x/oauth2" 34 | ) 35 | 36 | func SetEndpointUrl(rawText string) error { 37 | verified, err := verifyUrl(rawText) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | c, save, err := ReadWriteConfig() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | c.Url = verified 48 | 49 | return save() 50 | } 51 | 52 | func AddUser(rawText string) error { 53 | c, save, err := ReadWriteConfig() 54 | if err != nil { 55 | return err 56 | } 57 | if c.Users == nil { 58 | c.Users = make(map[string]bool) 59 | } 60 | 61 | list := strings.Split(rawText, ",") 62 | for _, input := range list { 63 | email, err := verifyEmail(input) 64 | if err != nil { 65 | return err 66 | } 67 | c.Users[email] = true 68 | } 69 | return save() 70 | } 71 | 72 | func RemoveUser(rawText string) error { 73 | c, save, err := ReadWriteConfig() 74 | if err != nil { 75 | return err 76 | } 77 | delete(c.Users, rawText) 78 | return save() 79 | } 80 | 81 | func RemoveService(i int) error { 82 | c, save, err := ReadWriteConfig() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | c.Services = append(c.Services[:i], c.Services[i+1:]...) 88 | return save() 89 | } 90 | 91 | func RemoveAccount(i int, s int) error { 92 | c, save, err := ReadWriteConfig() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | c.Services[s].Accounts = append(c.Services[s].Accounts[:i], c.Services[s].Accounts[i+1:]...) 98 | return save() 99 | } 100 | 101 | //////////////////////////////////////////////////////////////////////////////////////////////////// 102 | //////////////////////////////////////////////////////////////////////////////////////////////////// 103 | 104 | func NewServiceUpdater(previousName string) (*ServiceUpdater, error) { 105 | c, save, err := ReadWriteConfig() 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | return &ServiceUpdater{ 111 | c: c, 112 | previousName: previousName, 113 | save: save, 114 | }, nil 115 | } 116 | 117 | type ServiceUpdater struct { 118 | previousName string 119 | name, clientID, clientSecret, authURL, tokenURL, engineName *string 120 | scopes *[]string 121 | save func() error 122 | c *Config 123 | } 124 | 125 | func (u *ServiceUpdater) Commit() (interface{}, error) { 126 | var s *Service 127 | 128 | if u.previousName == "" { 129 | s = &Service{ 130 | OauthServiceCreds: &OauthServiceCreds{}, 131 | } 132 | u.c.Services = append(u.c.Services, s) 133 | } else { 134 | for _, other := range u.c.Services { 135 | if other.ServiceName == u.previousName { 136 | s = other 137 | } 138 | } 139 | } 140 | if s == nil { 141 | return nil, errors.New("Unable to find service, was its name changed while editing?") 142 | } 143 | 144 | if u.name != nil { 145 | s.ServiceName = *u.name 146 | } 147 | if u.clientID != nil { 148 | s.OauthServiceCreds.ClientID = *u.clientID 149 | } 150 | if u.clientSecret != nil { 151 | s.OauthServiceCreds.ClientSecret = *u.clientSecret 152 | } 153 | if u.authURL != nil { 154 | s.OauthServiceCreds.AuthURL = *u.authURL 155 | } 156 | if u.tokenURL != nil { 157 | s.OauthServiceCreds.TokenURL = *u.tokenURL 158 | } 159 | if u.scopes != nil { 160 | s.OauthServiceCreds.Scopes = *u.scopes 161 | } 162 | if u.engineName != nil { 163 | s.EngineName = *u.engineName 164 | } 165 | 166 | return s, u.save() 167 | } 168 | 169 | func (u *ServiceUpdater) Name(name string) error { 170 | err := verifyName(name) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | if name != u.previousName { 176 | for _, other := range u.c.Services { 177 | if name == other.ServiceName { 178 | return fmt.Errorf("The service name '%s' is already in use. Choose another.", name) 179 | } 180 | } 181 | } 182 | 183 | u.name = &name 184 | return nil 185 | } 186 | 187 | func (u *ServiceUpdater) ClientID(clientID string) error { 188 | if clientID == "" { 189 | return errors.New("Client ID cannot be empty.") 190 | } 191 | 192 | u.clientID = &clientID 193 | return nil 194 | } 195 | 196 | func (u *ServiceUpdater) ClientSecret(clientSecret string) error { 197 | if clientSecret == "" { 198 | return errors.New("Client Secret cannot be empty.") 199 | } 200 | 201 | u.clientSecret = &clientSecret 202 | return nil 203 | } 204 | 205 | func (u *ServiceUpdater) AuthURL(authURL string) error { 206 | verified, err := verifyUrl(authURL) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | u.authURL = &verified 212 | return nil 213 | } 214 | 215 | func (u *ServiceUpdater) TokenURL(tokenURL string) error { 216 | verified, err := verifyUrl(tokenURL) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | u.tokenURL = &verified 222 | return nil 223 | } 224 | 225 | func (u *ServiceUpdater) Scopes(scopes string) error { 226 | list := strings.Split(scopes, ",") 227 | output := make([]string, 0) 228 | 229 | for _, input := range list { 230 | scope := strings.TrimSpace(input) 231 | if scope != "" { 232 | output = append(output, scope) 233 | } 234 | } 235 | 236 | u.scopes = &output 237 | return nil 238 | } 239 | 240 | func (u *ServiceUpdater) EngineName(engineName string) error { 241 | u.engineName = &engineName 242 | return nil 243 | } 244 | 245 | //////////////////////////////////////////////////////////////////////////////////////////////////// 246 | //////////////////////////////////////////////////////////////////////////////////////////////////// 247 | 248 | func NewAccountUpdater(previousName string, s int) (*AccountUpdater, error) { 249 | c, save, err := ReadWriteConfig() 250 | if err != nil { 251 | return nil, err 252 | } 253 | 254 | return &AccountUpdater{ 255 | C: c, 256 | S: s, 257 | previousName: previousName, 258 | save: save, 259 | }, nil 260 | } 261 | 262 | type AccountUpdater struct { 263 | previousName string 264 | name, serviceURL *string 265 | oauthCreds *oauth2.Token 266 | clientCreds *ClientCreds 267 | save func() error 268 | S int 269 | C *Config 270 | } 271 | 272 | func (u *AccountUpdater) Commit() (interface{}, error) { 273 | var a *Account 274 | 275 | if u.previousName == "" { 276 | a = &Account{} 277 | u.C.Services[u.S].Accounts = append(u.C.Services[u.S].Accounts, a) 278 | } else { 279 | for _, other := range u.C.Services[u.S].Accounts { 280 | if other.AccountName == u.previousName { 281 | a = other 282 | } 283 | } 284 | } 285 | if a == nil { 286 | return nil, errors.New("Unable to find account, was its name changed while editing?") 287 | } 288 | 289 | if u.name != nil { 290 | a.AccountName = *u.name 291 | } 292 | 293 | if u.serviceURL != nil { 294 | a.ServiceURL = *u.serviceURL 295 | } 296 | 297 | if u.oauthCreds != nil { 298 | a.OauthAccountCreds = u.oauthCreds 299 | } 300 | 301 | if u.clientCreds != nil { 302 | a.ClientCreds = u.clientCreds 303 | } 304 | 305 | return a, u.save() 306 | } 307 | 308 | func (u *AccountUpdater) Name(name string) error { 309 | err := verifyName(name) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | if name != u.previousName { 315 | for _, other := range u.C.Services[u.S].Accounts { 316 | if name == other.AccountName { 317 | return fmt.Errorf("The account name '%s' is already in use. Choose another.", name) 318 | } 319 | } 320 | } 321 | 322 | u.name = &name 323 | return nil 324 | } 325 | 326 | func (u *AccountUpdater) ServiceURL(serviceURL string) error { 327 | verified, err := verifyUrl(serviceURL) 328 | if err != nil { 329 | return err 330 | } 331 | 332 | u.serviceURL = &verified 333 | return nil 334 | } 335 | 336 | func (u *AccountUpdater) ClientCreds() error { 337 | creds, err := generateNewClientCreds() 338 | if err != nil { 339 | return err 340 | } 341 | u.clientCreds = creds 342 | return nil 343 | } 344 | 345 | func (u *AccountUpdater) OauthCreds(code string) error { 346 | oauthConf, err := GenerateOauthConfig(u.C.Url, u.C.Services[u.S]) 347 | if err != nil { 348 | return err 349 | } 350 | token, err := oauthConf.Exchange(context.Background(), code) 351 | if err != nil { 352 | return err 353 | } 354 | 355 | u.oauthCreds = token 356 | return nil 357 | } 358 | 359 | //////////////////////////////////////////////////////////////////////////////////////////////////// 360 | //////////////////////////////////////////////////////////////////////////////////////////////////// 361 | 362 | func GenerateAccountKey(c *Config, s *Service, a *Account) (string, error) { 363 | j := struct { 364 | WebGatewayUrl string 365 | Protocol string 366 | PrivateKey string 367 | }{ 368 | WebGatewayUrl: c.Url, 369 | Protocol: a.ClientCreds.Protocol, 370 | PrivateKey: a.ClientCreds.PrivateKey, 371 | } 372 | 373 | b, err := json.Marshal(j) 374 | if err != nil { 375 | return "", err 376 | } 377 | 378 | inner := base64.StdEncoding.EncodeToString(b) 379 | 380 | return fmt.Sprintf("KEYBEGIN_%s/%s_%s_KEYEND", s.ServiceName, a.AccountName, inner), nil 381 | } 382 | 383 | func GenerateOauthConfig(url string, s *Service) (*oauth2.Config, error) { 384 | var endpoint = oauth2.Endpoint{ 385 | AuthURL: s.OauthServiceCreds.AuthURL, 386 | TokenURL: s.OauthServiceCreds.TokenURL, 387 | } 388 | redirectUrl, err := getRedirectUrl(url) 389 | if err != nil { 390 | return nil, err 391 | } 392 | oauthConf := &oauth2.Config{ 393 | ClientID: s.OauthServiceCreds.ClientID, 394 | ClientSecret: s.OauthServiceCreds.ClientSecret, 395 | Scopes: s.OauthServiceCreds.Scopes, 396 | Endpoint: endpoint, 397 | RedirectURL: redirectUrl, 398 | } 399 | return oauthConf, nil 400 | } 401 | 402 | func GenerateAuthUrl(oauthConf *oauth2.Config) (string, string, error) { 403 | state, err := generateRandomString() 404 | if err != nil { 405 | return "", "", fmt.Errorf("Problem with random number generation. Can't continue.\n") 406 | } 407 | return oauthConf.AuthCodeURL(state), state, nil 408 | } 409 | 410 | func VerifyState(code string, state string) (string, error) { 411 | jsonAuthCode, err := base64.StdEncoding.DecodeString(code) 412 | if err != nil { 413 | return "", fmt.Errorf("Bad decode.\n") 414 | } 415 | j := struct { 416 | Token string 417 | State string 418 | }{} 419 | json.Unmarshal(jsonAuthCode, &j) 420 | if j.State != state { 421 | return "", fmt.Errorf("Bad state. Expected %s, got %s\n", state, j.State) 422 | } 423 | return j.Token, nil 424 | } 425 | 426 | //////////////////////////////////////////////////////////////////////////////////////////////////// 427 | //////////////////////////////////////////////////////////////////////////////////////////////////// 428 | 429 | var rxName = regexp.MustCompile("^[-a-z0-9]+$") 430 | 431 | var rxEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+" + 432 | "@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]" + 433 | "(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") 434 | 435 | func verifyName(rawText string) error { 436 | if rawText == "" { 437 | return errors.New("Name cannot be empty.") 438 | } 439 | 440 | if !rxName.MatchString(rawText) { 441 | return fmt.Errorf("The name '%s' contains invalid characters.", rawText) 442 | } 443 | return nil 444 | } 445 | 446 | func verifyUrl(rawText string) (string, error) { 447 | tokenURL, err := url.ParseRequestURI(rawText) 448 | if err != nil { 449 | return "", errors.New(rawText + " is not a valid URL (include https://)") 450 | } 451 | if tokenURL.Scheme != "https" { 452 | return "", errors.New(rawText + " does not use https.") 453 | } 454 | 455 | return tokenURL.String(), nil 456 | } 457 | 458 | func verifyEmail(rawText string) (string, error) { 459 | email := strings.TrimSpace(rawText) 460 | if len(email) > 254 || !rxEmail.MatchString(email) { 461 | return "", fmt.Errorf("Email address '%s' is not valid.", email) 462 | } 463 | return email, nil 464 | } 465 | 466 | func getRedirectUrl(address string) (string, error) { 467 | redirectUrl, err := url.Parse(address) 468 | if err != nil { 469 | return "", errors.New("Web-Api-Gatway url setting is invalid, can't continue.") 470 | } 471 | redirectUrl.Path = "/authToken/" 472 | return redirectUrl.String(), nil 473 | } 474 | 475 | func generateRandomString() (string, error) { 476 | b := make([]byte, 30) 477 | _, err := rand.Read(b) 478 | if err != nil { 479 | return "", err 480 | } 481 | return fmt.Sprintf("%x", b), nil 482 | } 483 | 484 | func generateNewClientCreds() (*ClientCreds, error) { 485 | fmt.Println("Generating new secret for client credentials.\n") 486 | for i := 0; i < 10; i++ { 487 | 488 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 489 | if err != nil { 490 | fmt.Println("error generating key: ", err) 491 | fmt.Println("Trying again") 492 | continue 493 | } 494 | 495 | bytes, err := x509.MarshalPKCS8PrivateKey(privateKey) 496 | if err != nil { 497 | fmt.Println("error marshling key: ", err) 498 | fmt.Println("Trying again") 499 | continue 500 | } 501 | 502 | creds := &ClientCreds{ 503 | Protocol: "ECDSA_SHA256_PKCS8_V1", 504 | PrivateKey: base64.StdEncoding.EncodeToString(bytes), 505 | } 506 | return creds, nil 507 | } 508 | 509 | return nil, errors.New("Too many failures trying to create client credentials, exiting without saving.") 510 | } 511 | -------------------------------------------------------------------------------- /connectiontest/connectiontest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-api-gateway/69f8e6dcc56d769f9202b1468fc4ed2765262600/connectiontest/connectiontest -------------------------------------------------------------------------------- /connectiontest/connectiontest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "crypto/ecdsa" 22 | "crypto/rand" 23 | "crypto/sha256" 24 | "crypto/x509" 25 | "encoding/asn1" 26 | "encoding/base64" 27 | "encoding/json" 28 | "errors" 29 | "flag" 30 | "fmt" 31 | "math/big" 32 | "net" 33 | "net/http" 34 | "net/http/httputil" 35 | "path" 36 | "strings" 37 | "time" 38 | ) 39 | 40 | var invalidKey = errors.New( 41 | "Could not parse account key. It should look like: --accountKey=KEYBEGIN_service/account_abc123abc123abc123_KEYEND") 42 | 43 | var missingKey = errors.New( 44 | "accountKey flag is required. It should look like: --accountKey=KEYBEGIN_service/account_abc123abc123abc123_KEYEND") 45 | 46 | var invalidHeaderFlag = errors.New( 47 | "Could not parse headers. It should look like: \"someheader:value;otherheader:otherValue\"") 48 | 49 | func main() { 50 | o, err := getOptions() 51 | if err != nil { 52 | fmt.Println(err) 53 | return 54 | } 55 | 56 | accountKey, err := parseKey(o.accountKey) 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | 62 | privateKey, err := getPrivateKey(accountKey.PrivateKey) 63 | if err != nil { 64 | fmt.Println(err) 65 | return 66 | } 67 | 68 | t := createTransport(o.redirectAddr) 69 | 70 | fmt.Println("<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>") 71 | fmt.Println("<> Checking Web API Gateway status for given account key.") 72 | statusFullPath := path.Join("service", accountKey.service, "account", accountKey.account, "status") 73 | performRequest(accountKey, privateKey, t, statusFullPath, "GET", "", "") 74 | 75 | fmt.Println("<><><><><><><><><><><><><><><><><><><><><><><><><><><><><><>") 76 | fmt.Println("<> Performing specified connection test.") 77 | reqFullPath := path.Join("service", accountKey.service, "account", accountKey.account, "forward", o.path) 78 | performRequest(accountKey, privateKey, t, reqFullPath, o.method, o.body, o.headers) 79 | } 80 | 81 | func performRequest(accountKey *key, privateKey *ecdsa.PrivateKey, t http.RoundTripper, fullPath, method, body, headers string) { 82 | timestamp := fmt.Sprintf("%d", time.Now().Unix()) 83 | 84 | signature, err := getSignature(privateKey, fullPath, timestamp, body) 85 | if err != nil { 86 | fmt.Println(err) 87 | return 88 | } 89 | 90 | url := strings.TrimRight(accountKey.WebGatewayUrl, "/") + "/" + strings.TrimLeft(fullPath, "/") 91 | 92 | r, err := http.NewRequest(method, url, strings.NewReader(body)) 93 | if err != nil { 94 | fmt.Println(err) 95 | return 96 | } 97 | 98 | err = addHeaders(r.Header, signature, timestamp, headers) 99 | if err != nil { 100 | fmt.Println(err) 101 | return 102 | } 103 | 104 | d1, err := httputil.DumpRequestOut(r, true) 105 | if err != nil { 106 | fmt.Println(err) 107 | return 108 | } 109 | fmt.Println("===============================================================") 110 | fmt.Println("Request Sent") 111 | fmt.Println("================================") 112 | fmt.Printf("%s\n", d1) 113 | 114 | resp, err := t.RoundTrip(r) 115 | if err != nil { 116 | println("===============================") 117 | println("Error making request") 118 | println("=======================") 119 | fmt.Println(err) 120 | return 121 | } 122 | 123 | dump, err := httputil.DumpResponse(resp, true) 124 | if err != nil { 125 | println("===============================") 126 | println("Error reading response") 127 | println("=======================") 128 | fmt.Println(err) 129 | // Print what we can anyways 130 | } 131 | 132 | fmt.Println("===============================================================") 133 | fmt.Println("Response Recieved") 134 | fmt.Println("================================") 135 | fmt.Printf("%s\n", dump) 136 | } 137 | 138 | type options struct { 139 | path string 140 | accountKey string 141 | headers string 142 | body string 143 | method string 144 | redirectAddr string 145 | } 146 | 147 | func getOptions() (*options, error) { 148 | var o options 149 | flag.StringVar(&o.path, "path", "/", "The path after the domain in the url.") 150 | flag.StringVar(&o.accountKey, "accountKey", "", "Account key retrieved using the setup tool.") 151 | flag.StringVar(&o.headers, "headers", "", "how to use") 152 | flag.StringVar(&o.body, "body", "", "Body to send for PUT or POST methods.") 153 | flag.StringVar(&o.method, "method", "GET", "how to use") 154 | flag.StringVar(&o.redirectAddr, "redirectAddr", "", "how to use") 155 | flag.Parse() 156 | 157 | if o.accountKey == "" { 158 | return nil, missingKey 159 | } 160 | 161 | return &o, nil 162 | } 163 | 164 | type key struct { 165 | WebGatewayUrl string 166 | Protocol string 167 | PrivateKey string 168 | service string 169 | account string 170 | } 171 | 172 | func parseKey(accountKey string) (*key, error) { 173 | accountKey = strings.TrimSpace(accountKey) 174 | 175 | segments := strings.Split(accountKey, "_") 176 | if len(segments) != 4 || segments[0] != "KEYBEGIN" || segments[3] != "KEYEND" { 177 | return nil, invalidKey 178 | } 179 | 180 | b, err := base64.StdEncoding.DecodeString(segments[2]) 181 | if err != nil { 182 | return nil, invalidKey 183 | } 184 | 185 | var result key 186 | err = json.Unmarshal(b, &result) 187 | if err != nil { 188 | return nil, invalidKey 189 | } 190 | 191 | path := strings.Split(segments[1], "/") 192 | if len(path) != 2 { 193 | return nil, invalidKey 194 | } 195 | 196 | result.service = path[0] 197 | result.account = path[1] 198 | 199 | if result.WebGatewayUrl == "" || result.Protocol == "" || result.PrivateKey == "" { 200 | return nil, invalidKey 201 | } 202 | 203 | return &result, nil 204 | } 205 | 206 | func getPrivateKey(key string) (*ecdsa.PrivateKey, error) { 207 | fmt.Println("-------------------------=-=-=-=") 208 | fmt.Println(key) 209 | fmt.Println("-------------------------=-=-=-=") 210 | der, err := base64.StdEncoding.DecodeString(key) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | privateKey, err := x509.ParsePKCS8PrivateKey(der) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | switch privateKey := privateKey.(type) { 221 | case *ecdsa.PrivateKey: 222 | return privateKey, nil 223 | } 224 | 225 | return nil, errors.New("Private key not of type ecdsa") 226 | } 227 | 228 | func getSignature(privateKey *ecdsa.PrivateKey, url, timestamp, body string) (string, error) { 229 | signed := make([]byte, 0) 230 | signed = append(signed, []byte("/")...) 231 | signed = append(signed, []byte(strings.TrimLeft(url, "/"))...) 232 | signed = append(signed, []byte("\n")...) 233 | signed = append(signed, []byte(timestamp)...) 234 | signed = append(signed, []byte("\n")...) 235 | signed = append(signed, body...) 236 | 237 | hash := sha256.Sum256(signed) 238 | 239 | rawSig := struct { 240 | R, S *big.Int 241 | }{} 242 | 243 | var err error 244 | rawSig.R, rawSig.S, err = ecdsa.Sign(rand.Reader, privateKey, hash[:]) 245 | if err != nil { 246 | return "", err 247 | } 248 | 249 | b, err := asn1.Marshal(rawSig) 250 | if err != nil { 251 | return "", err 252 | } 253 | 254 | return base64.StdEncoding.EncodeToString(b), nil 255 | } 256 | 257 | func addHeaders(header http.Header, signature, timestamp, fromFlag string) error { 258 | header.Set("For-Web-Api-Gateway-Signature", signature) 259 | header.Set("For-Web-Api-Gateway-Request-Time-Utc", timestamp) 260 | if fromFlag == "" { 261 | return nil 262 | } 263 | 264 | pairs := strings.Split(fromFlag, ";") 265 | for _, pair := range pairs { 266 | split := strings.SplitN(pair, ":", 2) 267 | if len(split) != 2 { 268 | return invalidHeaderFlag 269 | } 270 | header.Set(split[0], split[1]) 271 | } 272 | return nil 273 | } 274 | 275 | func createTransport(redirectAddr string) http.RoundTripper { 276 | if redirectAddr == "" { 277 | return http.DefaultTransport 278 | } 279 | 280 | dialer := &net.Dialer{} 281 | 282 | dialFunc := func(ctx context.Context, network, addr string) (net.Conn, error) { 283 | return dialer.DialContext(ctx, network, redirectAddr) 284 | } 285 | 286 | return &http.Transport{ 287 | DialContext: dialFunc, 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/web-api-gateway 2 | 3 | require ( 4 | github.com/gofrs/uuid v3.2.0+incompatible 5 | github.com/gorilla/mux v1.7.3 6 | github.com/gorilla/securecookie v1.1.1 7 | github.com/gorilla/sessions v1.2.0 8 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 9 | google.golang.org/api v0.7.0 10 | ) 11 | 12 | go 1.13 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= 4 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 7 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 8 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 9 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 10 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 11 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 12 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 13 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 16 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 18 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 19 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 20 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 21 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 22 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 23 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 24 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 25 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 26 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 27 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 28 | github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= 29 | github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 30 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 31 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 32 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 33 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 34 | go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= 35 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 36 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 37 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 38 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 39 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 40 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 41 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 42 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 43 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 44 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 45 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= 46 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 47 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 48 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 50 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 52 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 53 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 54 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 55 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 58 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 61 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= 64 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 68 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 69 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 70 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 71 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 72 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 73 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 74 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 75 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 76 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 77 | google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= 78 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 79 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 80 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 81 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 82 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 83 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 84 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 85 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 86 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 87 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= 88 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 89 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 90 | google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= 91 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 92 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 93 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 94 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 95 | -------------------------------------------------------------------------------- /server/auth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "crypto/ecdsa" 22 | "crypto/sha256" 23 | "encoding/asn1" 24 | "encoding/base64" 25 | "io/ioutil" 26 | "math/big" 27 | "net/http" 28 | "strconv" 29 | "time" 30 | ) 31 | 32 | // As a matter of policy, changes to this file should be security reviewed, 33 | // while changes to other files are less likely to need it. 34 | 35 | func onlyAllowVerifiedRequests( 36 | handler http.Handler, key *ecdsa.PublicKey, now func() time.Time) http.HandlerFunc { 37 | 38 | return func(w http.ResponseWriter, r *http.Request) { 39 | signature, err := base64.StdEncoding.DecodeString(r.Header.Get("For-Web-Api-Gateway-Signature")) 40 | if err != nil { 41 | ErrorInvalidHeaders.ServeHTTP(w, r) 42 | return 43 | } 44 | 45 | type ecdsaSignature struct { 46 | R, S *big.Int 47 | } 48 | 49 | ecdsaSig := new(ecdsaSignature) 50 | if rest, err := asn1.Unmarshal(signature, ecdsaSig); err != nil || len(rest) != 0 { 51 | ErrorInvalidSignature.ServeHTTP(w, r) 52 | return 53 | } 54 | if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 { 55 | ErrorInvalidSignature.ServeHTTP(w, r) 56 | return 57 | } 58 | 59 | timestamp, err := strconv.ParseInt(r.Header.Get("For-Web-Api-Gateway-Request-Time-Utc"), 10, 64) 60 | if err != nil { 61 | ErrorInvalidHeaders.ServeHTTP(w, r) 62 | return 63 | } 64 | timeError := time.Unix(timestamp, 0).Sub(now()).Minutes() 65 | if timeError > 1 || timeError < -1 { 66 | ErrorInvalidTime.ServeHTTP(w, r) 67 | return 68 | } 69 | 70 | body, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | ErrorIO.ServeHTTP(w, r) 73 | return 74 | } 75 | 76 | signed := make([]byte, 0) 77 | signed = append(signed, []byte(r.URL.String())...) 78 | signed = append(signed, []byte("\n")...) 79 | signed = append(signed, []byte(r.Header.Get("For-Web-Api-Gateway-Request-Time-Utc"))...) 80 | signed = append(signed, []byte("\n")...) 81 | signed = append(signed, body...) 82 | 83 | hash := sha256.Sum256(signed) 84 | 85 | if !ecdsa.Verify(key, hash[:], ecdsaSig.R, ecdsaSig.S) { 86 | ErrorNotVerified.ServeHTTP(w, r) 87 | return 88 | } 89 | 90 | r2 := new(http.Request) 91 | *r2 = *r 92 | r2.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 93 | 94 | w.Header().Set("From-Web-Api-Gateway-Was-Auth-Error", "false") 95 | handler.ServeHTTP(w, r2) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /server/auth_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "crypto/ecdsa" 22 | "crypto/elliptic" 23 | "crypto/sha256" 24 | "encoding/asn1" 25 | "encoding/base64" 26 | "io/ioutil" 27 | "math/big" 28 | "math/rand" 29 | "net/http" 30 | "net/http/httptest" 31 | "testing" 32 | "time" 33 | ) 34 | 35 | var privateKey *ecdsa.PrivateKey 36 | 37 | var alternateNow = func() time.Time { 38 | return time.Unix(904867200, 0) 39 | } 40 | 41 | func init() { 42 | var err error 43 | // This is a horribly insecure way to create a key, only good for creating a 44 | // well known key for testing! 45 | privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.New(rand.NewSource(0))) 46 | if err != nil { 47 | panic(err) 48 | } 49 | } 50 | 51 | func createSignature(t *testing.T) string { 52 | digest := "https://www.proxy.com/some/path\n" + "904867200\n" + "request body" 53 | sum := sha256.Sum256([]byte(digest)) 54 | 55 | r, s, err := ecdsa.Sign(rand.New(rand.NewSource(0)), privateKey, sum[:]) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | return encodeAsn1Signature(r, s, t) 61 | } 62 | 63 | func encodeAsn1Signature(r, s *big.Int, t *testing.T) string { 64 | asn1Sig := struct { 65 | R, S *big.Int 66 | }{ 67 | r, s, 68 | } 69 | 70 | b, err := asn1.Marshal(asn1Sig) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | return base64.StdEncoding.EncodeToString(b) 76 | } 77 | 78 | func createRequest(t *testing.T) *http.Request { 79 | body := bytes.NewBuffer([]byte("request body")) 80 | 81 | r := httptest.NewRequest("POST", "https://www.proxy.com/some/path", body) 82 | r.Header.Set("For-Web-Api-Gateway-Signature", createSignature(t)) 83 | r.Header.Set("For-Web-Api-Gateway-Request-Time-Utc", "904867200") 84 | 85 | return r 86 | } 87 | 88 | func checkExpectations(t *testing.T, resp *http.Response, status int, body, wasError, errorCode string) { 89 | actualBody, _ := ioutil.ReadAll(resp.Body) 90 | 91 | if status != resp.StatusCode { 92 | t.Errorf("statusCode expected: %d actual: %d", status, resp.StatusCode) 93 | } 94 | if body != string(actualBody) { 95 | t.Errorf("body expected: %s actual: %s", body, string(actualBody)) 96 | } 97 | if wasError != resp.Header.Get("From-Web-Api-Gateway-Was-Error") { 98 | t.Errorf("From-Web-Api-Gateway-Was-Error expected: %s actual: %s", wasError, resp.Header.Get("From-Web-Api-Gateway-Was-Error")) 99 | } 100 | if errorCode != resp.Header.Get("From-Web-Api-Gateway-Error-Code") { 101 | t.Errorf("From-Web-Api-Gateway-Error-Code expected: %s actual: %s", errorCode, resp.Header.Get("From-Web-Api-Gateway-Error-Code")) 102 | } 103 | } 104 | 105 | func fatalHandler(t *testing.T) http.HandlerFunc { 106 | return func(w http.ResponseWriter, r *http.Request) { 107 | t.Error("The handler should not be called!") 108 | } 109 | } 110 | 111 | func TestGoodAuth(t *testing.T) { 112 | successHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 113 | body, err := ioutil.ReadAll(r.Body) 114 | if err != nil { 115 | t.Error(err) 116 | } 117 | if string(body) != "request body" { 118 | t.Errorf("request body expected: %s actual: %s", "request body", string(body)) 119 | } 120 | 121 | w.WriteHeader(http.StatusOK) 122 | w.Write([]byte("Success!")) 123 | }) 124 | 125 | handler := onlyAllowVerifiedRequests(successHandler, &privateKey.PublicKey, alternateNow) 126 | 127 | req := createRequest(t) 128 | w := httptest.NewRecorder() 129 | handler(w, req) 130 | 131 | checkExpectations(t, w.Result(), http.StatusOK, "Success!", "", "") 132 | } 133 | 134 | func TestNonAsn1Signature(t *testing.T) { 135 | handler := onlyAllowVerifiedRequests(fatalHandler(t), &privateKey.PublicKey, alternateNow) 136 | 137 | req := createRequest(t) 138 | req.Header.Set("For-Web-Api-Gateway-Signature", "thisisn'tgoingtowork") 139 | w := httptest.NewRecorder() 140 | handler(w, req) 141 | 142 | checkExpectations(t, w.Result(), http.StatusBadRequest, "", "true", "ErrorInvalidHeaders") 143 | } 144 | 145 | func TestNonNumericTimestamp(t *testing.T) { 146 | handler := onlyAllowVerifiedRequests(fatalHandler(t), &privateKey.PublicKey, alternateNow) 147 | 148 | req := createRequest(t) 149 | req.Header.Set("For-Web-Api-Gateway-Request-Time-Utc", "thisisn'tgoingtowork") 150 | w := httptest.NewRecorder() 151 | handler(w, req) 152 | 153 | checkExpectations(t, w.Result(), http.StatusBadRequest, "", "true", "ErrorInvalidHeaders") 154 | } 155 | 156 | func TestIncorrectSignature(t *testing.T) { 157 | handler := onlyAllowVerifiedRequests(fatalHandler(t), &privateKey.PublicKey, alternateNow) 158 | 159 | req := createRequest(t) 160 | sig := encodeAsn1Signature(big.NewInt(5), big.NewInt(6), t) 161 | req.Header.Set("For-Web-Api-Gateway-Signature", sig) 162 | w := httptest.NewRecorder() 163 | handler(w, req) 164 | 165 | checkExpectations(t, w.Result(), http.StatusUnauthorized, "", "true", "ErrorNotVerified") 166 | } 167 | 168 | func TestOldTimestamp(t *testing.T) { 169 | handler := onlyAllowVerifiedRequests(fatalHandler(t), &privateKey.PublicKey, func() time.Time { 170 | return time.Unix(904867200+61, 0) 171 | }) 172 | 173 | req := createRequest(t) 174 | w := httptest.NewRecorder() 175 | handler(w, req) 176 | 177 | checkExpectations(t, w.Result(), http.StatusBadRequest, "", "true", "ErrorInvalidTime") 178 | } 179 | 180 | func TestEarlyTimestamp(t *testing.T) { 181 | handler := onlyAllowVerifiedRequests(fatalHandler(t), &privateKey.PublicKey, func() time.Time { 182 | return time.Unix(904867200-61, 0) 183 | }) 184 | 185 | req := createRequest(t) 186 | w := httptest.NewRecorder() 187 | handler(w, req) 188 | 189 | checkExpectations(t, w.Result(), http.StatusBadRequest, "", "true", "ErrorInvalidTime") 190 | } 191 | -------------------------------------------------------------------------------- /server/errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import "net/http" 20 | 21 | type errorCode struct { 22 | httpStatus int 23 | s string 24 | } 25 | 26 | func (e *errorCode) String() string { 27 | return e.s 28 | } 29 | 30 | func (e *errorCode) ServeHTTP(w http.ResponseWriter, r *http.Request) { 31 | w.Header().Set("From-Web-Api-Gateway-Was-Error", "true") 32 | w.Header().Set("From-Web-Api-Gateway-Error-Code", e.String()) 33 | w.WriteHeader(e.httpStatus) 34 | } 35 | 36 | var ( 37 | ErrorInvalidHeaders = &errorCode{http.StatusBadRequest, "ErrorInvalidHeaders"} 38 | ErrorInvalidSignature = &errorCode{http.StatusBadRequest, "ErrorInvalidSignature"} 39 | ErrorInvalidTime = &errorCode{http.StatusBadRequest, "ErrorInvalidTime"} 40 | ErrorIO = &errorCode{http.StatusInternalServerError, "ErrorIO"} 41 | ErrorEncodingStatusJson = &errorCode{http.StatusInternalServerError, "ErrorEncodingStatusJson"} 42 | ErrorParsingRedirectUrl = &errorCode{http.StatusInternalServerError, "ErrorParsingRedirectUrl"} 43 | ErrorReadingConfig = &errorCode{http.StatusInternalServerError, "ErrorReadingConfig"} 44 | ErrorNotVerified = &errorCode{http.StatusUnauthorized, "ErrorNotVerified"} 45 | ) 46 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "crypto/ecdsa" 22 | "crypto/tls" 23 | "crypto/x509" 24 | "encoding/base64" 25 | "encoding/json" 26 | "errors" 27 | "flag" 28 | "fmt" 29 | "log" 30 | "net/http" 31 | "net/http/httputil" 32 | "net/url" 33 | "runtime" 34 | "strings" 35 | "sync" 36 | "time" 37 | 38 | "github.com/google/web-api-gateway/config" 39 | "golang.org/x/oauth2" 40 | ) 41 | 42 | const version = "2.1.1" 43 | 44 | // TODO: switch to some other time interval? 45 | const reloadInterval = time.Minute * 30 46 | 47 | var certFile *string = flag.String( 48 | "certFile", 49 | "/etc/webapigateway/cert/fullchain.pem", 50 | "This is the full public certificate for this web server.", 51 | ) 52 | 53 | var keyFile *string = flag.String( 54 | "keyFile", 55 | "/etc/webapigateway/cert/privkey.pem", 56 | "This is the private key for the certFile.", 57 | ) 58 | 59 | var addr *string = flag.String( 60 | "addr", 61 | ":443", 62 | "This is the address:port which the server listens to.", 63 | ) 64 | 65 | type Handler struct { 66 | http.HandlerFunc 67 | Enabled bool 68 | } 69 | 70 | type Handlers map[string]*Handler 71 | 72 | var ServerHandlers Handlers 73 | var muxer *http.ServeMux 74 | 75 | func main() { 76 | flag.Parse() 77 | 78 | log.Println("Reading config file...") 79 | log.Printf("Starting web-api-gateway, version %s\n", version) 80 | 81 | ServerHandlers = Handlers{} 82 | muxer = http.NewServeMux() 83 | ServerHandlers.HandleFunc("/authToken/", authTokenPage) 84 | ServerHandlers.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { 85 | fmt.Fprintf(w, "web-api-gateway version: %s\nGo version: %s", version, runtime.Version()) 86 | }) 87 | 88 | errHandler := createConfigHandler() 89 | ServerHandlers.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 90 | if errHandler != nil { 91 | errHandler.ServeHTTP(w, r) 92 | } 93 | }) 94 | 95 | cr, err := NewCertificateReloader(*certFile, *keyFile) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | server := &http.Server{ 101 | Addr: ":https", 102 | TLSConfig: &tls.Config{ 103 | GetCertificate: cr.GetCertificateFunc(), 104 | }, 105 | Handler: muxer, 106 | } 107 | log.Fatal(server.ListenAndServeTLS("", "")) 108 | } 109 | 110 | func (h Handlers) HandleFunc(pattern string, handler http.HandlerFunc) { 111 | _, contain := h[pattern] 112 | h[pattern] = &Handler{handler, true} 113 | if !contain { 114 | muxer.HandleFunc(pattern, h.ServeHTTP) 115 | } 116 | } 117 | 118 | func (h Handlers) ServeHTTP(w http.ResponseWriter, r *http.Request) { 119 | log.Printf("Incoming Request %s %s %s", r.RemoteAddr, r.Method, r.URL) 120 | path := r.URL.Path 121 | spath := strings.Split(path, "/") 122 | if len(spath) >= 6 { 123 | idx := strings.Index(path, spath[5]) 124 | path = path[:idx] 125 | } 126 | 127 | if strings.HasPrefix(path, "/portal") { 128 | UIHandlers().ServeHTTP(w, r) 129 | } else if handler, ok := h[path]; ok && handler.Enabled { 130 | handler.ServeHTTP(w, r) 131 | } else { 132 | http.Error(w, "Not Found", http.StatusNotFound) 133 | } 134 | } 135 | 136 | /////////////////////////////////////////////// 137 | type certificateReloader struct { 138 | sync.RWMutex 139 | cert *tls.Certificate 140 | certPath string 141 | keyPath string 142 | } 143 | 144 | func NewCertificateReloader(certPath, keyPath string) (*certificateReloader, error) { 145 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 146 | if err != nil { 147 | return nil, err 148 | } 149 | result := &certificateReloader{ 150 | certPath: certPath, 151 | keyPath: keyPath, 152 | } 153 | result.cert = &cert 154 | 155 | tickerChannel := time.NewTicker(reloadInterval).C 156 | 157 | go func() { 158 | for range tickerChannel { 159 | log.Printf("Reloading TLS certificate and key from %s and %s", certPath, keyPath) 160 | if err := result.maybeReload(); err != nil { 161 | log.Printf("Keeping old TLS certificate because the new one could not be loaded: %v", err) 162 | } 163 | } 164 | }() 165 | 166 | return result, nil 167 | } 168 | 169 | func (cr *certificateReloader) maybeReload() error { 170 | newCert, err := tls.LoadX509KeyPair(cr.certPath, cr.keyPath) 171 | if err != nil { 172 | return err 173 | } 174 | cr.Lock() 175 | defer cr.Unlock() 176 | cr.cert = &newCert 177 | 178 | log.Printf("Reloaded certificate successfully!") 179 | 180 | return nil 181 | } 182 | 183 | func (cr *certificateReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { 184 | return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 185 | cr.RLock() 186 | defer cr.RUnlock() 187 | return cr.cert, nil 188 | } 189 | } 190 | 191 | /////////////////////////////////////////// 192 | 193 | func createConfigHandler() http.Handler { 194 | c, err := config.ReadConfig() 195 | if err != nil { 196 | log.Printf("Error reading config file: %s", err) 197 | return ErrorReadingConfig 198 | } 199 | 200 | for _, service := range c.Services { 201 | for _, account := range service.Accounts { 202 | path, handler, err := createAccountHandler(c, service, account) 203 | if err != nil { 204 | log.Printf( 205 | "Error reading the config, service: %s, account: %s, error: %s", 206 | service.ServiceName, 207 | account.AccountName, 208 | err) 209 | return ErrorReadingConfig 210 | } 211 | ServerHandlers.HandleFunc(path, handler) 212 | } 213 | } 214 | 215 | return nil 216 | } 217 | 218 | func createAccountHandler(c *config.Config, service *config.Service, account *config.Account) (string, http.HandlerFunc, error) { 219 | // TODO: we're assuming that service and account names are valid. The editing tool validates this 220 | // but it should be validated when loading too. 221 | basePath := fmt.Sprintf("/service/%s/account/%s/", service.ServiceName, account.AccountName) 222 | mux := http.NewServeMux() 223 | 224 | modifyResponse, err := createModifyResponse(c.Url, basePath) 225 | if err != nil { 226 | return "", nil, err 227 | } 228 | 229 | { 230 | handler, err := createOAuthForwarder(service, account, modifyResponse) 231 | if err != nil { 232 | return "", nil, err 233 | } 234 | mux.Handle(basePath+"forward/", http.StripPrefix(basePath+"forward/", handler)) 235 | } 236 | 237 | mux.Handle(basePath+"authlessForward/", authlessForward(modifyResponse)) 238 | 239 | mux.Handle(basePath+"status", createStatusPage(service, account)) 240 | 241 | handler, err := wrapWithClientAuth(mux, account) 242 | if err != nil { 243 | return "", nil, err 244 | } 245 | return basePath, handler, nil 246 | } 247 | 248 | func wrapWithClientAuth(handler http.Handler, account *config.Account) (http.HandlerFunc, error) { 249 | // TODO TEST account.ClientCreds.Protocol and ensure it's what we're expecting here. 250 | 251 | der, err := base64.StdEncoding.DecodeString(account.ClientCreds.PrivateKey) 252 | if err != nil { 253 | // Don't log error details in case they include info on the secret. 254 | return nil, errors.New("error decoding private key from base64") 255 | } 256 | 257 | privateKey, err := x509.ParsePKCS8PrivateKey(der) 258 | if err != nil { 259 | // Don't log error details in case they include info on the secret. 260 | return nil, errors.New("error parsing private key") 261 | } 262 | 263 | switch privateKey := privateKey.(type) { 264 | case *ecdsa.PrivateKey: 265 | return onlyAllowVerifiedRequests(handler, &privateKey.PublicKey, time.Now), nil 266 | } 267 | return nil, errors.New("Private key not of type ecdsa") 268 | } 269 | 270 | func createModifyResponse(gatewayUrl, basePath string) (func(*http.Response) error, error) { 271 | if _, err := url.Parse(gatewayUrl); err != nil { 272 | return nil, err 273 | } 274 | 275 | return func(r *http.Response) error { 276 | if r.StatusCode >= 300 && r.StatusCode < 400 { 277 | location := r.Header.Get("Location") 278 | 279 | v := url.Values{} 280 | v.Set("url", location) 281 | 282 | newLocation, _ := url.Parse(gatewayUrl) 283 | newLocation.Path = basePath + "authlessForward/" 284 | newLocation.RawQuery = v.Encode() 285 | 286 | r.Header.Set("Location", newLocation.String()) 287 | } 288 | return nil 289 | }, nil 290 | } 291 | 292 | func createOAuthForwarder(service *config.Service, account *config.Account, modifyResponse func(*http.Response) error) (http.Handler, error) { 293 | var endpoint = oauth2.Endpoint{ 294 | AuthURL: service.OauthServiceCreds.AuthURL, 295 | TokenURL: service.OauthServiceCreds.TokenURL, 296 | } 297 | 298 | oauthConf := &oauth2.Config{ 299 | ClientID: service.OauthServiceCreds.ClientID, 300 | ClientSecret: service.OauthServiceCreds.ClientSecret, 301 | Scopes: service.OauthServiceCreds.Scopes, 302 | Endpoint: endpoint, 303 | } 304 | 305 | transport := &oauth2.Transport{ 306 | Source: oauthConf.TokenSource(context.Background(), account.OauthAccountCreds), 307 | } 308 | 309 | domain, err := url.Parse(account.ServiceURL) 310 | if err != nil { 311 | return nil, err 312 | } 313 | 314 | proxy := httputil.NewSingleHostReverseProxy(domain) 315 | proxy.Transport = transport 316 | proxy.ModifyResponse = modifyResponse 317 | 318 | fixRequest := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 319 | ClientIdHeader := r.Header.Get("For-Web-Api-Gateway-ClientID-Header") 320 | if ClientIdHeader != "" { 321 | r.Header[ClientIdHeader] = []string{service.OauthServiceCreds.ClientID} 322 | } 323 | 324 | adjustRequest(r, domain) 325 | 326 | proxy.ServeHTTP(w, r) 327 | }) 328 | 329 | return fixRequest, err 330 | } 331 | 332 | func adjustRequest(r *http.Request, domain *url.URL) { 333 | headersToRemove := []string{"User-Agent"} 334 | for header := range r.Header { 335 | if strings.HasPrefix(header, "For-Web-Api-Gateway") { 336 | headersToRemove = append(headersToRemove, header) 337 | } 338 | } 339 | for _, header := range headersToRemove { 340 | r.Header.Del(header) 341 | } 342 | 343 | r.RemoteAddr = "" 344 | r.Host = domain.Host 345 | 346 | } 347 | 348 | func authlessForward(modifyResponse func(*http.Response) error) http.Handler { 349 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 350 | r.ParseForm() 351 | 352 | var err error 353 | r.URL, err = url.Parse(r.FormValue("url")) 354 | if err != nil { 355 | ErrorParsingRedirectUrl.ServeHTTP(w, r) 356 | return 357 | } 358 | 359 | domain := url.URL{ 360 | Scheme: r.URL.Scheme, 361 | Host: r.URL.Host, 362 | } 363 | 364 | adjustRequest(r, &domain) 365 | 366 | proxy := httputil.NewSingleHostReverseProxy(&domain) 367 | proxy.ModifyResponse = modifyResponse 368 | 369 | proxy.ServeHTTP(w, r) 370 | }) 371 | } 372 | 373 | func createStatusPage(service *config.Service, account *config.Account) http.Handler { 374 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 375 | status := struct { 376 | Version string 377 | GoVersion string 378 | TokenUrl string 379 | AuthUrl string 380 | Scopes []string 381 | ServiceUrl string 382 | }{ 383 | Version: version, 384 | GoVersion: runtime.Version(), 385 | AuthUrl: service.OauthServiceCreds.AuthURL, 386 | TokenUrl: service.OauthServiceCreds.TokenURL, 387 | Scopes: service.OauthServiceCreds.Scopes, 388 | ServiceUrl: account.ServiceURL, 389 | } 390 | 391 | b, err := json.Marshal(status) 392 | if err != nil { 393 | ErrorEncodingStatusJson.ServeHTTP(w, r) 394 | return 395 | } 396 | 397 | _, err = w.Write(b) 398 | if err != nil { 399 | ErrorIO.ServeHTTP(w, r) 400 | return 401 | } 402 | }) 403 | } 404 | 405 | func authTokenPage(w http.ResponseWriter, r *http.Request) { 406 | err := r.ParseForm() 407 | if handleAuthTokenPageError(w, err) { 408 | return 409 | } 410 | 411 | if r.FormValue("error") != "" { 412 | _, err := fmt.Fprintf( 413 | w, 414 | "The authenticating service returned an error, code='%s', details='%s'.", 415 | r.FormValue("error"), 416 | r.FormValue("error_description")) 417 | 418 | handleAuthTokenPageError(w, err) 419 | return 420 | } 421 | 422 | response := struct { 423 | Token string 424 | State string 425 | }{ 426 | Token: r.FormValue("code"), 427 | State: r.FormValue("state"), 428 | } 429 | 430 | if response.Token == "" { 431 | _, err = fmt.Fprintf(w, "Missing required form value 'code'") 432 | handleAuthTokenPageError(w, err) 433 | return 434 | } 435 | 436 | if response.State == "" { 437 | _, err = fmt.Fprintf(w, "Missing required form value 'state'") 438 | handleAuthTokenPageError(w, err) 439 | return 440 | } 441 | 442 | b, err := json.Marshal(response) 443 | if handleAuthTokenPageError(w, err) { 444 | return 445 | } 446 | 447 | _, err = fmt.Fprintf( 448 | w, 449 | "Copy-paste this code into the setup tool: %s", 450 | base64.StdEncoding.EncodeToString(b)) 451 | 452 | if handleAuthTokenPageError(w, err) { 453 | return 454 | } 455 | } 456 | 457 | func handleAuthTokenPageError(w http.ResponseWriter, err error) bool { 458 | if err != nil { 459 | // Ignore error writing this out, we're already in a bad state. 460 | w.Write([]byte("Error generating response. It has been logged.")) 461 | log.Println("Error generating authToken response:", err) 462 | return true 463 | } 464 | return false 465 | } 466 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "io/ioutil" 21 | "net/http/httptest" 22 | "testing" 23 | ) 24 | 25 | func performTest(t *testing.T, parameters, expected string) { 26 | r := httptest.NewRequest("GET", "https://www.proxy.com/authToken/?"+parameters, nil) 27 | w := httptest.NewRecorder() 28 | 29 | authTokenPage(w, r) 30 | 31 | b, _ := ioutil.ReadAll(w.Body) 32 | actual := string(b) 33 | 34 | if expected != actual { 35 | t.Errorf("body expected: %s actual: %s", expected, actual) 36 | } 37 | } 38 | 39 | func TestAuthTokenPage(t *testing.T) { 40 | performTest(t, "", "Missing required form value 'code'") 41 | performTest(t, "code=123", "Missing required form value 'state'") 42 | performTest(t, "code=123&state=456", "Copy-paste this code into the setup tool: eyJUb2tlbiI6IjEyMyIsIlN0YXRlIjoiNDU2In0=") 43 | performTest(t, "error=foo&error_description=not%20this", "The authenticating service returned an error, code='foo', details='not this'.") 44 | } 45 | -------------------------------------------------------------------------------- /server/static/gateway.css: -------------------------------------------------------------------------------- 1 | 2 | /* HEADER NAVIGATION */ 3 | .navbar { 4 | min-height: 48px; 5 | padding: 0 12px; 6 | border-radius: 0; 7 | } 8 | 9 | .navbar-default { 10 | border-color: rgba(0,0,0,0); 11 | background-color: #fff; 12 | box-shadow: 0 3px 4px 0 rgba(0,0,0,.2), 0 3px 3px -2px rgba(0,0,0,.14), 0 1px 8px 0 rgba(0,0,0,.12); 13 | } 14 | .navbar-default .navbar-brand, 15 | .navbar-default .navbar-brand:hover, 16 | .navbar-default .navbar-text, 17 | .navbar-default .navbar-nav>li>a { 18 | color: rgba(0,0,0,0.87); 19 | } 20 | 21 | .navbar-default .navbar-brand { 22 | padding: 14px 15px; 23 | } 24 | .navbar-default .navbar-nav>li>a { 25 | text-transform: uppercase; 26 | font-size: 13px; 27 | } 28 | .navbar-default .navbar-nav>li>a:hover { 29 | background-color: transparent; 30 | } 31 | .navbar-default .navbar-form { 32 | margin: 10px 0; 33 | } 34 | .login-link, 35 | .login-link:hover { 36 | font-size: 12px; 37 | padding: 5px 10px; 38 | text-decoration: none; 39 | color: rgba(0,0,0,0.87); 40 | border-radius: 4px; 41 | border: 1px solid rgba(0,0,0,.26); 42 | } 43 | 44 | /* MAIN CONTENT AREA */ 45 | body { 46 | font-family: "Roboto","Helvetica","Arial",sans-serif; 47 | } 48 | 49 | .container { 50 | padding: 0; 51 | } 52 | 53 | h3 { 54 | color: rgba(0,0,0,.87); 55 | font-family: "Roboto","Helvetica","Arial",sans-serif; 56 | font-size: 20px; 57 | font-weight: 500; 58 | letter-spacing: normal; 59 | line-height: 28px; 60 | margin-bottom: 16px; 61 | margin-top: 0; 62 | } 63 | .container h3 { 64 | height: auto; 65 | padding: 0 8px 0 0; 66 | display: inline-block!important; 67 | vertical-align: middle; 68 | } 69 | .btn { 70 | font-size: 12px; 71 | padding: 5px 10px; 72 | } 73 | .btn-primary, 74 | .btn-primary:hover { 75 | background-color: #4284f4; 76 | border-color: transparent; 77 | } 78 | .btn-default, 79 | .btn-default:hover { 80 | color: rgba(0,0,0,.87); 81 | background-color: #fff; 82 | border-color: rgba(0,0,0,.26); 83 | } 84 | .glyphicon { 85 | display: none; 86 | } 87 | .welcomepage { 88 | width: 745px; 89 | margin: 40px auto; 90 | } 91 | 92 | /* TABLE */ 93 | .addservice { 94 | margin-bottom: 16px; 95 | } 96 | .container div ul { 97 | margin: 32px 0 10px 0; 98 | padding: 0; 99 | } 100 | .container div ul li { 101 | list-style: none; 102 | } 103 | .container div ul li h4 { 104 | display: inline; 105 | vertical-align: middle; 106 | padding: 0 8px 0 0; 107 | } 108 | .table tr th { 109 | font-size: 12px; 110 | } 111 | .table tr td { 112 | font-size: 12px; 113 | background-color: rgba(250,250,250,1); 114 | } 115 | .table tr.accounts td { 116 | padding-left: 24px; 117 | background-color: rgba(255,255,255,1); 118 | } 119 | .table tr.accounts td:last-child { 120 | padding-left: 103px; 121 | } 122 | 123 | /* Form styles */ 124 | .form-group { 125 | margin-bottom: 24px; 126 | } 127 | .container form { 128 | margin-top: 16px; 129 | } 130 | .container .form-group img { 131 | width: 18px; 132 | opacity: 0.54; 133 | vertical-align: top; 134 | } 135 | 136 | /* Authorized users */ 137 | .authorizedusers .form-group { 138 | display: inline-block; 139 | vertical-align: text-bottom; 140 | } 141 | 142 | .authorizedusers .form-group .btn-success { 143 | padding: 4px 12px; 144 | } 145 | 146 | /* Snackbar */ 147 | .alert { 148 | width: 100%; 149 | position: absolute; 150 | bottom: 0; 151 | margin: 0; 152 | align-items: center; 153 | border-radius: 2px; 154 | color: #fff; 155 | height: 48px; 156 | padding: 0 24px; 157 | background: #323232; 158 | border: none; 159 | box-shadow: 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12), 0px 3px 5px -1px rgba(0, 0, 0, 0.2); 160 | } 161 | .alert strong { 162 | display: inline-block; 163 | padding: 16px 0; 164 | font-weight: normal; 165 | } 166 | @media (min-width: 768px) { 167 | .alert { 168 | width: 750px; 169 | } 170 | } 171 | @media (min-width: 992px) { 172 | .alert { 173 | width: 970px; 174 | } 175 | } 176 | @media (min-width: 1200px) { 177 | .alert { 178 | width: 1170px; 179 | } 180 | } 181 | 182 | /* Dialog styles */ 183 | dialog { 184 | padding: 24px; 185 | border: none; 186 | border-radius: 2px; 187 | box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); 188 | } 189 | dialog::backdrop { 190 | background-color: rgba(33, 33, 33, 0.48); 191 | } 192 | -------------------------------------------------------------------------------- /server/static/tooltip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/static/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-api-gateway/69f8e6dcc56d769f9202b1468fc4ed2765262600/server/static/welcome.png -------------------------------------------------------------------------------- /server/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "html/template" 23 | "io/ioutil" 24 | "net/http" 25 | ) 26 | 27 | var templatesFolder *string = flag.String( 28 | "templatesfolder", 29 | "/go/src/github.com/google/web-api-gateway/server/templates/", 30 | "This is the path for the templates folder.", 31 | ) 32 | 33 | // parseTemplate applies a given file to the body of the base template. 34 | func parseTemplate(filename string) *appTemplate { 35 | tmpl := template.Must(template.ParseFiles(*templatesFolder + "base.html")) 36 | tmpl.New("body").Parse("\n") 37 | if filename != "" { 38 | b, err := ioutil.ReadFile(filename) 39 | if err != nil { 40 | panic(fmt.Errorf("could not read template: %v", err)) 41 | } 42 | template.Must(tmpl.Lookup("body").Parse(string(b))) 43 | } 44 | 45 | return &appTemplate{tmpl.Lookup("base.html")} 46 | } 47 | 48 | // appTemplate is a user login-aware wrapper for a html/template. 49 | type appTemplate struct { 50 | t *template.Template 51 | } 52 | 53 | // Execute writes the template using the provided data, adding login and user 54 | // information to the base template. 55 | func (tmpl *appTemplate) Execute(w http.ResponseWriter, r *http.Request, data interface{}) *appError { 56 | d := struct { 57 | Data interface{} 58 | Profile *profile 59 | Flash string 60 | }{ 61 | Data: data, 62 | } 63 | d.Profile = profileFromSession(r) 64 | d.Flash = flashFromSession(w, r) 65 | if err := tmpl.t.Execute(w, d); err != nil { 66 | return appErrorf(err, "could not write template: %v", err) 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /server/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {{/* 3 | Copyright 2019 Google LLC 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */}} 17 | 18 | 19 | Web API Gateway UI 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 49 |
50 | {{if .Flash}} 51 | 54 | {{end}} 55 | {{template "body" .Data}} 56 |
57 | {{else}} 58 | 61 | 62 | 63 |
64 | {{if .Flash}} 65 | 68 | {{end}} 69 |
70 | 71 |
72 |

Welcome to the Web API Gateway!


73 |

Web API Gateway allows you to share API access with partners.

74 |

Please Log in first.

75 |

Please have the Config file ready.

76 | Welcome! 77 |
78 | {{end}} 79 | 80 | 81 | -------------------------------------------------------------------------------- /server/templates/editAccount.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 |

{{if and .Account .Url}}Reauthorize{{else if .Url}}Add{{else}}Edit{{end}} Account

17 | 18 |
19 | 20 | 22 | {{if and .Account .Url}} 23 | {{else}} 24 |
25 | 36 |
37 | {{end}} 38 | 39 |
40 | 42 | 45 | 46 | Name should only consist of lowercase letters, numbers and hyphens. 47 | 48 |
49 | {{if .Url}} 50 |
51 | 52 | Authenticate URL 53 |
54 |
55 | 56 | 57 |
58 | {{end}} 59 |
60 | 61 |
62 |
63 | 64 | -------------------------------------------------------------------------------- /server/templates/editService.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 |

{{if .Service}}Edit{{else}}Add{{end}} Service

17 | 18 |
19 | 21 | {{if .Service}} 22 | {{else}} 23 |
24 | 30 |
31 | {{end}} 32 | 33 |
34 | 35 | 38 | 39 | Name should only consist of lowercase letters, numbers and hyphens. 40 | 41 |
42 |
43 | 47 | 49 |
50 |
51 | 55 | 57 |
58 |
59 | 60 |
61 |
62 | -------------------------------------------------------------------------------- /server/templates/key.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 |

The Account Key

17 |
Please copy and paste everything from (and including) KEYBEGIN to KEYEND 18 |

19 |
20 | 23 | -------------------------------------------------------------------------------- /server/templates/list.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 | {{if not .Services}} 17 |
18 |

Welcome to the Web API Gateway!


19 |

Web API Gateway allows you to share API access with partners.

20 |

Start by "Adding a service".

21 | {{else}} 22 |

Services

23 | {{end}} 24 | 29 | {{if not .Services}} 30 | Welcome! 31 |
32 | {{else}} 33 | 34 | 35 | 36 | 37 | 38 | {{end}} 39 | {{range .Services}} 40 | {{$ServiceName := .ServiceName}} 41 | 42 | 43 | 64 | 65 | {{range .Accounts}} 66 | 67 | 68 | 96 | 97 | 98 | 106 | {{end}} 107 | 115 | {{end}} 116 |
Services/AccountsActions
{{$ServiceName}} 44 | 45 | Add Account 46 | 47 | 48 | Edit 49 | 50 | 51 |

Remove service

52 |

Removing a service will delete all accounts created under this service. 53 | Please review and confirm your request.

54 | Remove {{$ServiceName}} 55 |

56 | Save 57 | 59 |
60 | 63 |
{{.AccountName}} 69 | 71 | Edit 72 | 73 | 74 |

Remove account

75 |

Removing an account will break existing connections. Please 76 | review and confirm your request.

77 | Remove {{.AccountName}} 78 |

79 | Save 81 | 83 |
84 | 87 | 89 | Get Account Key 90 | 91 | 93 | Reauthorize 94 | 95 |
117 | -------------------------------------------------------------------------------- /server/templates/upload.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 |

Upload Config File

17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /server/templates/user.html: -------------------------------------------------------------------------------- 1 | {{/* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */}} 16 |

Authorized UI Users

17 |
Please add emails of authorized gateway users.
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | {{range $key, $v := .}} 34 | 35 | 38 | 49 | 50 | 58 | {{end}} 59 |
EmailsActions
36 | {{$key}} 37 | 39 | 40 |

Remove user

41 |

Please review and confirm your request.

42 | Remove {{$key}} 43 |

44 | Save 45 | 46 |
47 | 48 |
60 | -------------------------------------------------------------------------------- /server/templates/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-api-gateway/69f8e6dcc56d769f9202b1468fc4ed2765262600/server/templates/welcome.png -------------------------------------------------------------------------------- /server/ui.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/gob" 23 | "encoding/json" 24 | "fmt" 25 | "io" 26 | "log" 27 | "net/http" 28 | "net/url" 29 | "strings" 30 | 31 | uuid "github.com/gofrs/uuid" 32 | option "google.golang.org/api/option" 33 | plus "google.golang.org/api/plus/v1" 34 | 35 | "github.com/google/web-api-gateway/config" 36 | "github.com/gorilla/mux" 37 | "github.com/gorilla/securecookie" 38 | "github.com/gorilla/sessions" 39 | "golang.org/x/oauth2" 40 | "golang.org/x/oauth2/google" 41 | ) 42 | 43 | const ( 44 | defaultSessionID = "default" 45 | profileSessionKey = "profile" 46 | oauthTokenSessionKey = "oauth_token" 47 | stateSessionKey = "state" 48 | oauthFlowRedirectKey = "redirect" 49 | ) 50 | 51 | var ( 52 | listTmpl = parseTemplate(*templatesFolder + "list.html") 53 | editServiceTmpl = parseTemplate(*templatesFolder + "editService.html") 54 | editAccountTmpl = parseTemplate(*templatesFolder + "editAccount.html") 55 | keyTmpl = parseTemplate(*templatesFolder + "key.html") 56 | userTmpl = parseTemplate(*templatesFolder + "user.html") 57 | uploadTmpl = parseTemplate(*templatesFolder + "upload.html") 58 | ) 59 | 60 | var oauthConf *oauth2.Config = &oauth2.Config{ 61 | ClientID: "523939206127-3pr1qbrn0g78l6r9nu10l733q9obgn0t.apps.googleusercontent.com", 62 | ClientSecret: "zKY48Os4L8xKAuQoiBFqrLkW", 63 | Scopes: []string{"email", "profile"}, 64 | Endpoint: google.Endpoint, 65 | } 66 | 67 | var cookieStore = createStore() 68 | 69 | var engineMap = make(map[string]*config.Engine) 70 | 71 | type data struct { 72 | Service *config.Service 73 | Account *config.Account 74 | Url string 75 | Template *config.Template 76 | Domains []*config.Domain 77 | } 78 | 79 | type profile struct { 80 | ID, DisplayName, ImageURL string 81 | Emails []*plus.PersonEmails 82 | } 83 | 84 | func init() { 85 | gob.Register(&oauth2.Token{}) 86 | gob.Register(&profile{}) 87 | 88 | createMapping() 89 | } 90 | 91 | func UIHandlers() *mux.Router { 92 | r := mux.NewRouter() 93 | 94 | r.Handle("/portal", http.RedirectHandler("/portal/", http.StatusFound)) 95 | 96 | r.Methods("GET").Path("/portal/").Handler(appHandler(listHandler)) 97 | r.Methods("GET").Path("/portal/addservice").Handler(appHandler(addServiceHandler)) 98 | r.Methods("GET").Path("/portal/editservice/{service}").Handler(appHandler(editServiceHandler)) 99 | r.Methods("GET").Path("/portal/removeservice/{service}").Handler(appHandler(removeServiceHandler)) 100 | 101 | r.Methods("GET").Path("/portal/addaccount/{service}").Handler(appHandler(addAccountHandler)) 102 | r.Methods("GET").Path("/portal/editaccount/{service}/{account}").Handler(appHandler(editAccountHandler)) 103 | r.Methods("GET").Path("/portal/removeaccount/{service}/{account}").Handler(appHandler(removeAccountHandler)) 104 | r.Methods("GET").Path("/portal/retrievekey/{service}/{account}").Handler(appHandler(retrieveKeyHandler)) 105 | r.Methods("GET").Path("/portal/reauthorizeaccount/{service}/{account}").Handler(appHandler(reauthorizeAccountHandler)) 106 | 107 | r.Methods("POST").Path("/portal/saveservice").Handler(appHandler(saveServiceHandler)) 108 | r.Methods("POST").Path("/portal/saveaccount").Handler(appHandler(saveAccountHandler)) 109 | 110 | r.Methods("GET").Path("/portal/login").Handler(appHandler(loginHandler)) 111 | r.Methods("GET").Path("/portal/auth").Handler(appHandler(oauthCallbackHandler)) 112 | r.Methods("POST").Path("/portal/logout").Handler(appHandler(logoutHandler)) 113 | 114 | r.Methods("GET").Path("/portal/users").Handler(appHandler(listUserHandler)) 115 | r.Methods("POST").Path("/portal/adduser").Handler(appHandler(addUserHandler)) 116 | r.Methods("GET").Path("/portal/removeuser/{user}").Handler(appHandler(removeUserHandler)) 117 | 118 | r.Methods("GET").Path("/portal/upload").Handler(appHandler(uploadHandler)) 119 | r.Methods("POST").Path("/portal/mapping").Handler(appHandler(mappingHandler)) 120 | 121 | r.PathPrefix("/portal/static/").Handler(http.StripPrefix("/portal/static/", 122 | http.FileServer(http.Dir("/go/src/github.com/google/web-api-gateway/server/static")))) 123 | 124 | return r 125 | } 126 | 127 | func loginHandler(w http.ResponseWriter, r *http.Request) *appError { 128 | c, err := config.ReadConfig() 129 | if err != nil { 130 | return appErrorf(err, "could not read config file: %v", err) 131 | } 132 | redirectUrl, err := url.Parse(c.Url) 133 | if err != nil { 134 | return appErrorf(err, "could not parse URL: %v", err) 135 | } 136 | 137 | redirectUrl.Path = "/portal/auth" 138 | oauthConf.RedirectURL = redirectUrl.String() 139 | 140 | sessionID := uuid.Must(uuid.NewV4()).String() 141 | oauthFlowSession, err := cookieStore.New(r, sessionID) 142 | if err != nil { 143 | return appErrorf(err, "could not create oauth session: %v", err) 144 | } 145 | oauthFlowSession.Options.MaxAge = 10 * 60 // 10 minutes 146 | oauthFlowSession.Values[oauthFlowRedirectKey] = redirectUrl.Path 147 | if err := oauthFlowSession.Save(r, w); err != nil { 148 | return appErrorf(err, "could not save session: %v", err) 149 | } 150 | 151 | url := oauthConf.AuthCodeURL(sessionID, oauth2.ApprovalForce) 152 | http.Redirect(w, r, url, http.StatusFound) 153 | return nil 154 | } 155 | 156 | func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) *appError { 157 | oauthFlowSession, err := cookieStore.Get(r, r.FormValue("state")) 158 | if err != nil { 159 | return appErrorf(err, "invalid state parameter. try logging in again.") 160 | } 161 | 162 | // ? 163 | redirectURL, ok := oauthFlowSession.Values[oauthFlowRedirectKey].(string) 164 | if !ok || strings.Compare(r.URL.Path, redirectURL) != 0 { 165 | return &appError{Message: "The callback is suspicious."} 166 | } 167 | 168 | ctx := context.Background() 169 | code := r.FormValue("code") 170 | tok, err := oauthConf.Exchange(ctx, code) 171 | if err != nil { 172 | return appErrorf(err, "could not get auth token: %v", err) 173 | } 174 | // if browser saved an old session with name "default", the err here will 175 | // not be nil, but this is ok, so no need to check on err 176 | session, _ := cookieStore.New(r, defaultSessionID) 177 | plusService, err := plus.NewService(ctx, option.WithTokenSource(oauthConf.TokenSource(ctx, tok))) 178 | if err != nil { 179 | return appErrorf(err, "could not get plus service: %v", err) 180 | } 181 | person, err := plusService.People.Get("me").Do() 182 | if err != nil { 183 | return appErrorf(err, "could not fetch Google profiles: %v", err) 184 | } 185 | profile := stripProfile(person) 186 | 187 | c, err := config.ReadConfig() 188 | if err != nil { 189 | return appErrorf(err, "could not read config file: %v", err) 190 | } 191 | 192 | emailValue := profile.Emails[0].Value 193 | if c.Users[emailValue] { 194 | session.Values[oauthTokenSessionKey] = tok 195 | session.Values[profileSessionKey] = profile 196 | if err := session.Save(r, w); err != nil { 197 | return appErrorf(err, "could not save session: %v", err) 198 | } 199 | } else { 200 | session.AddFlash("Sorry you are not authorized, please contact IT department.") 201 | if err := session.Save(r, w); err != nil { 202 | return appErrorf(err, "could not save session: %v", err) 203 | } 204 | } 205 | 206 | http.Redirect(w, r, "/portal/", http.StatusFound) 207 | return nil 208 | } 209 | 210 | func logoutHandler(w http.ResponseWriter, r *http.Request) *appError { 211 | session, err := cookieStore.Get(r, defaultSessionID) 212 | if err != nil { 213 | return appErrorf(err, "could not get default session: %v", err) 214 | } 215 | session.Options.MaxAge = -1 // Clear session. 216 | if err := session.Save(r, w); err != nil { 217 | return appErrorf(err, "could not save session: %v", err) 218 | } 219 | http.Redirect(w, r, "/portal/", http.StatusFound) 220 | return nil 221 | } 222 | 223 | func listHandler(w http.ResponseWriter, r *http.Request) *appError { 224 | c, err := config.ReadConfig() 225 | if err != nil { 226 | return appErrorf(err, "could not read config file: %v", err) 227 | } 228 | return listTmpl.Execute(w, r, *c) 229 | } 230 | 231 | func editServiceHandler(w http.ResponseWriter, r *http.Request) *appError { 232 | return editHandler(w, r, editServiceTmpl) 233 | } 234 | 235 | func editAccountHandler(w http.ResponseWriter, r *http.Request) *appError { 236 | return editHandler(w, r, editAccountTmpl) 237 | } 238 | 239 | func addServiceHandler(w http.ResponseWriter, r *http.Request) *appError { 240 | tmp, err := config.ReadTemplate() 241 | if err != nil { 242 | return appErrorf(err, "could not read template: %v", err) 243 | } 244 | 245 | if tmp == nil || len(tmp.Engines) == 0 { 246 | http.Redirect(w, r, "/portal/upload", http.StatusFound) 247 | } 248 | return editServiceTmpl.Execute(w, r, data{nil, nil, "", tmp, nil}) 249 | } 250 | 251 | func addAccountHandler(w http.ResponseWriter, r *http.Request) *appError { 252 | c, err := config.ReadConfig() 253 | if err != nil { 254 | return appErrorf(err, "could not read config file: %v", err) 255 | } 256 | tmp, err := config.ReadTemplate() 257 | if err != nil { 258 | return appErrorf(err, "could not read template: %v", err) 259 | } 260 | serviceStr := mux.Vars(r)["service"] 261 | _, service, err := serviceFromRequest(serviceStr, c) 262 | if err != nil { 263 | return appErrorf(err, "could not find service: %v", err) 264 | } 265 | oauthConf, err := config.GenerateOauthConfig(c.Url, service) 266 | if err != nil { 267 | return appErrorf(err, "could not get Oauth config: %v", err) 268 | } 269 | authUrl, state, err := config.GenerateAuthUrl(oauthConf) 270 | if err != nil { 271 | return appErrorf(err, "could not generate auth URL: %v", err) 272 | } 273 | if err := setStateToSession(w, r, state); err != nil { 274 | return err 275 | } 276 | engine, ok := engineMap[service.EngineName] 277 | if !ok { 278 | return appErrorf(err, "could not get engine: %v", err) 279 | } 280 | return editAccountTmpl.Execute(w, r, data{service, nil, authUrl, tmp, engine.Domains}) 281 | } 282 | 283 | func saveServiceHandler(w http.ResponseWriter, r *http.Request) *appError { 284 | name := r.FormValue("PreviousServiceName") 285 | u, err := config.NewServiceUpdater(name) 286 | if err != nil { 287 | return appErrorf(err, "could not get service updater: %v", err) 288 | } 289 | 290 | session, e := cookieStore.Get(r, defaultSessionID) 291 | if e != nil { 292 | return appErrorf(e, "could not get default session: %v", e) 293 | } 294 | 295 | if err := u.Name(r.FormValue("ServiceName")); err != nil { 296 | session.AddFlash(fmt.Sprintf("%v", err)) 297 | if err := session.Save(r, w); err != nil { 298 | return appErrorf(err, "could not save session: %v", err) 299 | } 300 | if name == "" { 301 | http.Redirect(w, r, "/portal/addservice", http.StatusFound) 302 | return nil 303 | } else { 304 | http.Redirect(w, r, fmt.Sprintf("/portal/editservice/%s", name), http.StatusFound) 305 | return nil 306 | } 307 | } 308 | 309 | u.ClientID(r.FormValue("ClientID")) 310 | u.ClientSecret(r.FormValue("ClientSecret")) 311 | 312 | engineName := r.FormValue("Engine") 313 | if engineName != "" { 314 | engine, err := engineFromRequest(engineName) 315 | if err != nil { 316 | return appErrorf(err, "could not find engine", err) 317 | } 318 | u.AuthURL(engine.AuthURL) 319 | u.TokenURL(engine.TokenURL) 320 | u.Scopes(engine.Scopes) 321 | u.EngineName(engineName) 322 | } 323 | 324 | if _, err := u.Commit(); err != nil { 325 | return appErrorf(err, "could not save changes: %v", err) 326 | } 327 | session.AddFlash(fmt.Sprintf("Successfully saved changes for service %s.", r.FormValue("ServiceName"))) 328 | if err := session.Save(r, w); err != nil { 329 | return appErrorf(err, "could not save session: %v", err) 330 | } 331 | http.Redirect(w, r, "/portal/", http.StatusFound) 332 | return nil 333 | } 334 | 335 | func saveAccountHandler(w http.ResponseWriter, r *http.Request) *appError { 336 | sName := r.FormValue("ServiceName") 337 | previousAccount := r.FormValue("PreviousAccountName") 338 | 339 | c, err := config.ReadConfig() 340 | if err != nil { 341 | return appErrorf(err, "could not read config file: %v", err) 342 | } 343 | idx, _, err := serviceFromRequest(sName, c) 344 | if err != nil { 345 | return appErrorf(err, "could not find service: %v", err) 346 | } 347 | u, err := config.NewAccountUpdater(previousAccount, idx) 348 | if err != nil { 349 | return appErrorf(err, "could not get account updater: %v", err) 350 | } 351 | 352 | session, e := cookieStore.Get(r, defaultSessionID) 353 | if e != nil { 354 | return appErrorf(e, "could not get default session: %v", e) 355 | } 356 | if err := u.Name(r.FormValue("AccountName")); err != nil { 357 | session.AddFlash(fmt.Sprintf("%v", err)) 358 | if err := session.Save(r, w); err != nil { 359 | return appErrorf(err, "could not save session: %v", err) 360 | } 361 | if previousAccount == "" { 362 | http.Redirect(w, r, fmt.Sprintf("/portal/addaccount/%s", sName), http.StatusFound) 363 | return nil 364 | } else { 365 | http.Redirect(w, r, 366 | fmt.Sprintf("/portal/editaccount/%s/%s", sName, previousAccount), 367 | http.StatusFound) 368 | return nil 369 | } 370 | } 371 | 372 | domainName := r.FormValue("Domain") 373 | if domainName != "" { 374 | u.ServiceURL(domainName) 375 | } 376 | 377 | s := session.Values[stateSessionKey] 378 | if s != nil { 379 | state, ok := s.(string) 380 | if !ok { 381 | return &appError{Message: "could not get state"} 382 | } 383 | code := r.FormValue("Code") 384 | if code != "" { 385 | decode, err := config.VerifyState(code, state) 386 | if err != nil { 387 | session.AddFlash("Oauth failed, please try again.") 388 | if err := session.Save(r, w); err != nil { 389 | return appErrorf(err, "could not save session: %v", err) 390 | } 391 | if previousAccount == "" { 392 | http.Redirect(w, r, fmt.Sprintf("/portal/addaccount/%s", sName), http.StatusFound) 393 | return nil 394 | } else { 395 | http.Redirect(w, r, 396 | fmt.Sprintf("/portal/reauthorizeaccount/%s/%s", sName, previousAccount), 397 | http.StatusFound) 398 | return nil 399 | } 400 | } 401 | 402 | if err := u.OauthCreds(decode); err != nil { 403 | return appErrorf(err, "could not update Oauth credentials: %v", err) 404 | } 405 | } 406 | } 407 | 408 | session.Values[stateSessionKey] = nil 409 | if err := session.Save(r, w); err != nil { 410 | return appErrorf(err, "could not save session: %v", err) 411 | } 412 | 413 | if previousAccount == "" { 414 | if err := u.ClientCreds(); err != nil { 415 | return appErrorf(err, "could not generate client credentails: %v", err) 416 | } 417 | } 418 | 419 | i, err := u.Commit() 420 | if err != nil { 421 | return appErrorf(err, "could not save changes: %v", err) 422 | } 423 | a, ok := i.(*config.Account) 424 | if !ok { 425 | return appErrorf(err, "could not get account: %v", err) 426 | } 427 | 428 | // add new handler 429 | path, handler, err := createAccountHandler(u.C, u.C.Services[u.S], a) 430 | if err != nil { 431 | return appErrorf(err, "could not add handler for service: %s, account: %s, error: %v", 432 | u.C.Services[u.S].ServiceName, a.AccountName, err) 433 | } 434 | ServerHandlers.HandleFunc(path, handler) 435 | 436 | session.AddFlash(fmt.Sprintf("Successfully saved changes for account %s.", a.AccountName)) 437 | if err := session.Save(r, w); err != nil { 438 | return appErrorf(err, "could not save session: %v", err) 439 | } 440 | 441 | http.Redirect(w, r, "/portal/", http.StatusFound) 442 | return nil 443 | } 444 | 445 | func removeServiceHandler(w http.ResponseWriter, r *http.Request) *appError { 446 | c, err := config.ReadConfig() 447 | if err != nil { 448 | return appErrorf(err, "could not read config file: %v", err) 449 | } 450 | 451 | serviceStr := mux.Vars(r)["service"] 452 | i, _, err := serviceFromRequest(serviceStr, c) 453 | if err != nil { 454 | return appErrorf(err, "could not find service: %v", err) 455 | } 456 | 457 | if err := config.RemoveService(i); err != nil { 458 | return appErrorf(err, "could not delete service: %v", err) 459 | } 460 | 461 | session, _ := cookieStore.Get(r, defaultSessionID) 462 | session.AddFlash(fmt.Sprintf("Successfully removed service %s.", serviceStr)) 463 | if err := session.Save(r, w); err != nil { 464 | return appErrorf(err, "could not save session: %v", err) 465 | } 466 | 467 | http.Redirect(w, r, "/portal/", http.StatusFound) 468 | return nil 469 | } 470 | 471 | func removeAccountHandler(w http.ResponseWriter, r *http.Request) *appError { 472 | c, err := config.ReadConfig() 473 | if err != nil { 474 | return appErrorf(err, "could not read config file: %v", err) 475 | } 476 | 477 | serviceStr := mux.Vars(r)["service"] 478 | sIdx, service, err := serviceFromRequest(serviceStr, c) 479 | if err != nil { 480 | return appErrorf(err, "could not find service: %v", err) 481 | } 482 | 483 | accountStr := mux.Vars(r)["account"] 484 | i, account, err := accountFromRequest(accountStr, service) 485 | if err != nil { 486 | return appErrorf(err, "could not find account: %v", err) 487 | } 488 | 489 | if err := config.RemoveAccount(i, sIdx); err != nil { 490 | return appErrorf(err, "could not delete account: %v", err) 491 | } 492 | 493 | // disable handler 494 | basePath := fmt.Sprintf("/service/%s/account/%s/", service.ServiceName, account.AccountName) 495 | ServerHandlers[basePath].Enabled = false 496 | 497 | session, _ := cookieStore.Get(r, defaultSessionID) 498 | session.AddFlash(fmt.Sprintf("Successfully removed account %s.", accountStr)) 499 | if err := session.Save(r, w); err != nil { 500 | return appErrorf(err, "could not save session: %v", err) 501 | } 502 | 503 | http.Redirect(w, r, "/portal/", http.StatusFound) 504 | return nil 505 | } 506 | 507 | func removeUserHandler(w http.ResponseWriter, r *http.Request) *appError { 508 | userStr := mux.Vars(r)["user"] 509 | if err := config.RemoveUser(userStr); err != nil { 510 | return appErrorf(err, "could not delete user: %v", err) 511 | } 512 | 513 | session, _ := cookieStore.Get(r, defaultSessionID) 514 | session.AddFlash(fmt.Sprintf("Successfully removed user %s.", userStr)) 515 | if err := session.Save(r, w); err != nil { 516 | return appErrorf(err, "could not save session: %v", err) 517 | } 518 | 519 | http.Redirect(w, r, "/portal/users", http.StatusFound) 520 | return nil 521 | } 522 | 523 | func editHandler(w http.ResponseWriter, r *http.Request, tmpl *appTemplate) *appError { 524 | c, err := config.ReadConfig() 525 | if err != nil { 526 | return appErrorf(err, "could not read config file: %v", err) 527 | } 528 | 529 | serviceStr := mux.Vars(r)["service"] 530 | _, service, err := serviceFromRequest(serviceStr, c) 531 | if err != nil { 532 | return appErrorf(err, "could not find service: %v", err) 533 | } 534 | if tmpl == editServiceTmpl { 535 | return tmpl.Execute(w, r, data{service, nil, "", nil, nil}) 536 | } else { 537 | accountStr := mux.Vars(r)["account"] 538 | _, account, err := accountFromRequest(accountStr, service) 539 | if err != nil { 540 | return appErrorf(err, "could not find account: %v", err) 541 | } 542 | engine, ok := engineMap[service.EngineName] 543 | if !ok { 544 | return appErrorf(err, "could not get engine: %v", err) 545 | } 546 | return tmpl.Execute(w, r, data{service, account, "", nil, engine.Domains}) 547 | } 548 | } 549 | 550 | func retrieveKeyHandler(w http.ResponseWriter, r *http.Request) *appError { 551 | c, err := config.ReadConfig() 552 | if err != nil { 553 | return appErrorf(err, "could not read config file: %v", err) 554 | } 555 | 556 | serviceStr := mux.Vars(r)["service"] 557 | _, service, err := serviceFromRequest(serviceStr, c) 558 | if err != nil { 559 | return appErrorf(err, "could not find service: %v", err) 560 | } 561 | accountStr := mux.Vars(r)["account"] 562 | _, account, err := accountFromRequest(accountStr, service) 563 | if err != nil { 564 | return appErrorf(err, "could not find account: %v", err) 565 | } 566 | key, err := config.GenerateAccountKey(c, service, account) 567 | if err != nil { 568 | return appErrorf(err, "could not create account key: %v", err) 569 | } 570 | return keyTmpl.Execute(w, r, key) 571 | } 572 | 573 | func reauthorizeAccountHandler(w http.ResponseWriter, r *http.Request) *appError { 574 | c, err := config.ReadConfig() 575 | if err != nil { 576 | return appErrorf(err, "could not read config file: %v", err) 577 | } 578 | 579 | serviceStr := mux.Vars(r)["service"] 580 | _, service, err := serviceFromRequest(serviceStr, c) 581 | if err != nil { 582 | return appErrorf(err, "could not find service: %v", err) 583 | } 584 | accountStr := mux.Vars(r)["account"] 585 | _, account, err := accountFromRequest(accountStr, service) 586 | if err != nil { 587 | return appErrorf(err, "could not find account: %v", err) 588 | } 589 | oauthConf, err := config.GenerateOauthConfig(c.Url, service) 590 | if err != nil { 591 | return appErrorf(err, "could not get Oauth config: %v", err) 592 | } 593 | authUrl, state, err := config.GenerateAuthUrl(oauthConf) 594 | if err != nil { 595 | return appErrorf(err, "could not generate auth URL: %v", err) 596 | } 597 | if err := setStateToSession(w, r, state); err != nil { 598 | return err 599 | } 600 | return editAccountTmpl.Execute(w, r, data{service, account, authUrl, nil, nil}) 601 | } 602 | 603 | func listUserHandler(w http.ResponseWriter, r *http.Request) *appError { 604 | c, err := config.ReadConfig() 605 | if err != nil { 606 | return appErrorf(err, "could not read config file: %v", err) 607 | } 608 | return userTmpl.Execute(w, r, c.Users) 609 | } 610 | 611 | func addUserHandler(w http.ResponseWriter, r *http.Request) *appError { 612 | email := r.FormValue("Email") 613 | if err := config.AddUser(email); err != nil { 614 | return appErrorf(err, "could not add user: %v", err) 615 | } 616 | http.Redirect(w, r, "/portal/users", http.StatusFound) 617 | return nil 618 | } 619 | 620 | func uploadHandler(w http.ResponseWriter, r *http.Request) *appError { 621 | return uploadTmpl.Execute(w, r, nil) 622 | } 623 | 624 | func mappingHandler(w http.ResponseWriter, r *http.Request) *appError { 625 | f, _, err := r.FormFile("Mapping") 626 | if err != nil { 627 | http.Redirect(w, r, "/portal/upload", http.StatusFound) 628 | return nil 629 | } 630 | defer f.Close() 631 | var buf bytes.Buffer 632 | var t config.Template 633 | if _, err := io.Copy(&buf, f); err != nil { 634 | return appErrorf(err, "could not read file: %v", err) 635 | } 636 | err = json.Unmarshal(buf.Bytes(), &t) 637 | if err != nil { 638 | return appErrorf(err, "could not parse file: %v", err) 639 | } 640 | 641 | c, save, err := config.ReadWriteConfig() 642 | if err != nil { 643 | return appErrorf(err, "could not read config file", err) 644 | } 645 | 646 | c.Template = t 647 | if err = save(); err != nil { 648 | return appErrorf(err, "could not save to config file", err) 649 | } 650 | 651 | createMapping() 652 | 653 | http.Redirect(w, r, "/portal/", http.StatusFound) 654 | return nil 655 | } 656 | 657 | func serviceFromRequest(serviceStr string, c *config.Config) (int, *config.Service, error) { 658 | for i, s := range c.Services { 659 | if serviceStr == s.ServiceName { 660 | return i, s, nil 661 | } 662 | } 663 | return -1, nil, fmt.Errorf("No such service: %s", serviceStr) 664 | } 665 | 666 | func accountFromRequest(accountStr string, s *config.Service) (int, *config.Account, error) { 667 | for i, a := range s.Accounts { 668 | if accountStr == a.AccountName { 669 | return i, a, nil 670 | } 671 | } 672 | return -1, nil, fmt.Errorf("No such account: %s", accountStr) 673 | } 674 | 675 | func engineFromRequest(engineStr string) (*config.Engine, error) { 676 | tmp, err := config.ReadTemplate() 677 | if err != nil { 678 | return nil, fmt.Errorf("Could not read template") 679 | } 680 | 681 | for _, e := range tmp.Engines { 682 | if engineStr == e.EngineName { 683 | return e, nil 684 | } 685 | } 686 | return nil, fmt.Errorf("No such engine: %s", engineStr) 687 | } 688 | 689 | func stripProfile(p *plus.Person) *profile { 690 | return &profile{ 691 | Emails: p.Emails, 692 | DisplayName: p.DisplayName, 693 | ImageURL: p.Image.Url, 694 | } 695 | } 696 | 697 | func createStore() *sessions.CookieStore { 698 | store := sessions.NewCookieStore(securecookie.GenerateRandomKey(32)) 699 | store.Options = &sessions.Options{ 700 | Secure: true, 701 | Path: "/", 702 | MaxAge: 86400 * 7, // TODO: change to another duration? 703 | HttpOnly: true, 704 | } 705 | return store 706 | } 707 | 708 | func createMapping() { 709 | tmp, err := config.ReadTemplate() 710 | if err == nil { 711 | for _, engine := range tmp.Engines { 712 | engineMap[engine.EngineName] = engine 713 | } 714 | } 715 | } 716 | 717 | func profileFromSession(r *http.Request) *profile { 718 | session, err := cookieStore.Get(r, defaultSessionID) 719 | if err != nil { 720 | return nil 721 | } 722 | tok, ok := session.Values[oauthTokenSessionKey].(*oauth2.Token) 723 | if !ok || !tok.Valid() { 724 | return nil 725 | } 726 | profile, ok := session.Values[profileSessionKey].(*profile) 727 | if !ok { 728 | return nil 729 | } 730 | return profile 731 | } 732 | 733 | func flashFromSession(w http.ResponseWriter, r *http.Request) string { 734 | session, err := cookieStore.Get(r, defaultSessionID) 735 | if err != nil { 736 | return "" 737 | } 738 | var flash string 739 | var ok bool 740 | flashes := session.Flashes() 741 | if len(flashes) > 0 { 742 | if flash, ok = flashes[0].(string); !ok { 743 | return "" 744 | } 745 | } 746 | if err := session.Save(r, w); err != nil { 747 | return "" 748 | } 749 | return flash 750 | } 751 | 752 | func setStateToSession(w http.ResponseWriter, r *http.Request, state string) *appError { 753 | session, err := cookieStore.Get(r, defaultSessionID) 754 | if err != nil { 755 | return appErrorf(err, "could not get default session: %v", err) 756 | } 757 | session.Values[stateSessionKey] = state 758 | if err := session.Save(r, w); err != nil { 759 | return appErrorf(err, "could not save session: %v", err) 760 | } 761 | return nil 762 | } 763 | 764 | type appHandler func(http.ResponseWriter, *http.Request) *appError 765 | 766 | type appError struct { 767 | Error error 768 | Message string 769 | Code int 770 | } 771 | 772 | func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 773 | if e := fn(w, r); e != nil { // e is *appError, not os.Error. 774 | log.Printf("Handler error: status code: %d, message: %s, underlying err: %#v", 775 | e.Code, e.Message, e.Error) 776 | 777 | http.Error(w, e.Message, e.Code) 778 | } 779 | } 780 | 781 | func appErrorf(err error, format string, v ...interface{}) *appError { 782 | return &appError{ 783 | Error: err, 784 | Message: fmt.Sprintf(format, v...), 785 | Code: 500, 786 | } 787 | } 788 | -------------------------------------------------------------------------------- /setuptool/setuptool.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // The command line tool for setting up services and accounts. 18 | package main 19 | 20 | import ( 21 | "bufio" 22 | "flag" 23 | "fmt" 24 | "os" 25 | "strconv" 26 | "strings" 27 | 28 | "github.com/google/web-api-gateway/config" 29 | ) 30 | 31 | func main() { 32 | flag.Parse() 33 | fmt.Printf("Welcome to the Web Api Gateway Config Tool.\n\n") 34 | 35 | c, _, err := config.ReadWriteConfig() 36 | if err != nil { 37 | fmt.Printf("Unable to load config file %v\n", err) 38 | os.Exit(1) 39 | } 40 | 41 | term := newRealTerm() 42 | 43 | if c.Url == "" { 44 | editUrl(term) 45 | } 46 | 47 | takeActionLoop(term, backIsExit, 48 | newAction("Retrieve Account Key", retrieveAccountKey), 49 | newAction("Edit Web Api Gateway Url", editUrl), 50 | newAction("Add Service", addService), 51 | newAction("Edit Service", editService), 52 | newAction("Delete Service", removeService), 53 | newAction("Add Account", addAccount), 54 | newAction("Edit Account", editAccount), 55 | newAction("Delete Account", removeAccount), 56 | newAction("Add authorized UI users (email address)", addUser), 57 | newAction("Delete authorized UI users (email address)", removeUser), 58 | ) 59 | } 60 | 61 | //////////////////////////////////////////////////////////////////////////////////////////////////// 62 | //////////////////////////////////////////////////////////////////////////////////////////////////// 63 | 64 | type commiter interface { 65 | Commit() (interface{}, error) 66 | } 67 | 68 | func commit(t *term, c commiter) { 69 | fmt.Println("Saving...") 70 | 71 | for { 72 | _, err := c.Commit() 73 | if err == nil { 74 | fmt.Println("Save successful!") 75 | fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 76 | fmt.Println("Remember to restart the server.") 77 | fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 78 | return 79 | } 80 | fmt.Printf("There was an error saving the config file: %v\n", err) 81 | tryAgain := t.readBoolean("Do you want to try again? (no to discard unsaved changes.)") 82 | if !tryAgain { 83 | return 84 | } 85 | } 86 | } 87 | 88 | //////////////////////////////////////////////////////////////////////////////////////////////////// 89 | //////////////////////////////////////////////////////////////////////////////////////////////////// 90 | 91 | func editUrl(t *term) { 92 | userInput(t, "Enter the Web Api Gateway Url> ", config.SetEndpointUrl) 93 | } 94 | 95 | func ServiceName(u *config.ServiceUpdater, t *term) { 96 | userInput(t, "Choose a name (only use lowercase letters, numbers, and dashes)> ", u.Name) 97 | } 98 | 99 | func ServiceClientID(u *config.ServiceUpdater, t *term) { 100 | userInput(t, "Enter the Oauth Client ID> ", u.ClientID) 101 | } 102 | 103 | func ServiceClientSecret(u *config.ServiceUpdater, t *term) { 104 | userInput(t, "Enter the Oauth Client Secret> ", u.ClientSecret) 105 | } 106 | 107 | func ServiceAuthURL(u *config.ServiceUpdater, t *term) { 108 | userInput(t, "Enter the Oauth Auth URL> ", u.AuthURL) 109 | } 110 | 111 | func ServiceTokenURL(u *config.ServiceUpdater, t *term) { 112 | userInput(t, "Enter the Oauth Token URL> ", u.TokenURL) 113 | } 114 | 115 | func ServiceScopes(u *config.ServiceUpdater, t *term) { 116 | userInput(t, "Enter the Oauth scopes> ", u.Scopes) 117 | } 118 | 119 | func AccountName(u *config.AccountUpdater, t *term) { 120 | userInput(t, "Choose a name (only use lowercase letters, numbers, and dashes)> ", u.Name) 121 | } 122 | 123 | func AccountServiceURL(u *config.AccountUpdater, t *term) { 124 | userInput(t, "Enter the Service URL> ", u.ServiceURL) 125 | } 126 | 127 | func AccountOauthCreds(u *config.AccountUpdater, t *term) bool { 128 | oauthConf, err := config.GenerateOauthConfig(u.C.Url, u.C.Services[u.S]) 129 | if err != nil { 130 | return false 131 | } 132 | for { 133 | authUrl, state, err := config.GenerateAuthUrl(oauthConf) 134 | if err != nil { 135 | fmt.Println(err) 136 | return false 137 | } 138 | fmt.Println("Please go to this url and authorize the application:") 139 | fmt.Println(authUrl) 140 | fmt.Printf("Enter the code here> ") 141 | encodedAuthCode := t.readSimpleString() 142 | decodedToken, err := config.VerifyState(encodedAuthCode, state) 143 | 144 | if err != nil { 145 | fmt.Println(err) 146 | continue 147 | } 148 | if err = u.OauthCreds(decodedToken); err == nil { 149 | return true 150 | } 151 | fmt.Println(err) 152 | fmt.Println("\nPlease try again.") 153 | } 154 | } 155 | 156 | func AccountClientCreds(u *config.AccountUpdater, t *term) { 157 | if err := u.ClientCreds(); err != nil { 158 | fmt.Println(err) 159 | } 160 | } 161 | 162 | func addService(t *term) { 163 | u, err := config.NewServiceUpdater("") 164 | if err != nil { 165 | fmt.Println(err) 166 | return 167 | } 168 | 169 | ServiceName(u, t) 170 | ServiceClientID(u, t) 171 | ServiceClientSecret(u, t) 172 | ServiceAuthURL(u, t) 173 | ServiceTokenURL(u, t) 174 | ServiceScopes(u, t) 175 | 176 | commit(t, u) 177 | } 178 | 179 | func serviceCurry(f func(*config.ServiceUpdater, *term), u *config.ServiceUpdater) func(*term) { 180 | return func(t *term) { 181 | f(u, t) 182 | } 183 | } 184 | 185 | func editService(t *term) { 186 | _, name := chooseService(t) 187 | if name == "" { 188 | return 189 | } 190 | u, err := config.NewServiceUpdater(name) 191 | if err != nil { 192 | fmt.Println(err) 193 | return 194 | } 195 | 196 | takeActionLoop(t, backIsBack, 197 | newAction("Edit name", confirmRename(serviceCurry(ServiceName, u))), 198 | newAction("Edit Client Id", serviceCurry(ServiceClientID, u)), 199 | newAction("Edit Client Secret", serviceCurry(ServiceClientSecret, u)), 200 | newAction("Edit Auth Url", serviceCurry(ServiceAuthURL, u)), 201 | newAction("Edit Token Url", serviceCurry(ServiceTokenURL, u)), 202 | newAction("Edit Scopes", serviceCurry(ServiceScopes, u))) 203 | 204 | commit(t, u) 205 | } 206 | 207 | func removeService(t *term) { 208 | idx, name := chooseService(t) 209 | if idx == -1 { 210 | return 211 | } 212 | if config.RemoveService(idx) != nil { 213 | fmt.Println("Error deleting service: " + name) 214 | return 215 | } 216 | } 217 | 218 | func addAccount(t *term) { 219 | idx, _ := chooseService(t) 220 | if idx == -1 { 221 | return 222 | } 223 | u, err := config.NewAccountUpdater("", idx) 224 | if err != nil { 225 | fmt.Println(err) 226 | return 227 | } 228 | 229 | AccountName(u, t) 230 | AccountServiceURL(u, t) 231 | AccountClientCreds(u, t) 232 | 233 | if !AccountOauthCreds(u, t) { 234 | fmt.Println("Could not add account") 235 | return 236 | } 237 | commit(t, u) 238 | } 239 | 240 | func accountCurry(f func(*config.AccountUpdater, *term), u *config.AccountUpdater) func(*term) { 241 | return func(t *term) { 242 | f(u, t) 243 | } 244 | } 245 | 246 | func accountCurry2(f func(*config.AccountUpdater, *term) bool, u *config.AccountUpdater) func(*term) { 247 | return func(t *term) { 248 | f(u, t) 249 | } 250 | } 251 | 252 | func editAccount(t *term) { 253 | idx, _ := chooseService(t) 254 | if idx == -1 { 255 | return 256 | } 257 | _, name := chooseAccount(t, idx) 258 | if name == "" { 259 | return 260 | } 261 | u, err := config.NewAccountUpdater(name, idx) 262 | if err != nil { 263 | fmt.Println(err) 264 | return 265 | } 266 | 267 | takeActionLoop(t, backIsBack, 268 | newAction("Edit name", confirmRename(accountCurry(AccountName, u))), 269 | newAction("Edit service Url", accountCurry(AccountServiceURL, u)), 270 | newAction("Generate New Client Credentials", confirmNewClientCredentials(accountCurry(AccountClientCreds, u))), 271 | newAction("Reauthorzie account", accountCurry2(AccountOauthCreds, u))) 272 | 273 | commit(t, u) 274 | } 275 | 276 | func removeAccount(t *term) { 277 | s, _ := chooseService(t) 278 | if s == -1 { 279 | return 280 | } 281 | a, name := chooseAccount(t, s) 282 | if a == -1 { 283 | return 284 | } 285 | if config.RemoveAccount(s, a) != nil { 286 | fmt.Println("Error deleting account: " + name) 287 | return 288 | } 289 | } 290 | 291 | func retrieveAccountKey(t *term) { 292 | c, err := config.ReadConfig() 293 | if err != nil { 294 | fmt.Println("Unable to read config", err) 295 | return 296 | } 297 | 298 | type accountSpecific struct { 299 | s *config.Service 300 | a *config.Account 301 | } 302 | 303 | allAccounts := make([]accountSpecific, 0) 304 | 305 | for _, s := range c.Services { 306 | for _, a := range s.Accounts { 307 | allAccounts = append(allAccounts, accountSpecific{s, a}) 308 | } 309 | } 310 | 311 | if len(allAccounts) == 0 { 312 | fmt.Println("You must create accounts first!") 313 | return 314 | } 315 | 316 | fmt.Println("Select which account:") 317 | namer := func(i int) string { 318 | return allAccounts[i].s.ServiceName + "/" + allAccounts[i].a.AccountName 319 | } 320 | i := t.readChoice(namer, len(allAccounts)) 321 | 322 | key, err := config.GenerateAccountKey(c, allAccounts[i].s, allAccounts[i].a) 323 | if err != nil { 324 | fmt.Println("Error creating account key:") 325 | fmt.Println(err) 326 | return 327 | } 328 | fmt.Println() 329 | fmt.Println("Copy and paste everything from (and including) KEYBEGIN to KEYEND") 330 | fmt.Println() 331 | fmt.Println(key) 332 | fmt.Println() 333 | } 334 | 335 | func addUser(t *term) { 336 | userInput(t, "Enter the users' emails> ", config.AddUser) 337 | fmt.Println("User added.\n") 338 | } 339 | 340 | func removeUser(t *term) { 341 | userInput(t, "Enter the users' email> ", config.RemoveUser) 342 | fmt.Println("User removed.\n") 343 | } 344 | 345 | func chooseService(t *term) (int, string) { 346 | c, err := config.ReadConfig() 347 | if err != nil { 348 | fmt.Println("Unable to read config", err) 349 | return -1, "" 350 | } 351 | 352 | if len(c.Services) == 0 { 353 | fmt.Println("There are no services.") 354 | return -1, "" 355 | } 356 | 357 | fmt.Println("Services:") 358 | 359 | var names []string 360 | 361 | for _, s := range c.Services { 362 | names = append(names, s.ServiceName) 363 | } 364 | 365 | namer := func(i int) string { return names[i] } 366 | idx := t.readChoice(namer, len(c.Services)) 367 | return idx, names[idx] 368 | } 369 | 370 | func chooseAccount(t *term, s int) (int, string) { 371 | c, err := config.ReadConfig() 372 | if err != nil { 373 | fmt.Println("Unable to read config", err) 374 | return -1, "" 375 | } 376 | 377 | if len(c.Services[s].Accounts) == 0 { 378 | fmt.Println("There are no accounts.\n") 379 | return -1, "" 380 | } 381 | 382 | fmt.Println("Accounts:") 383 | 384 | var names []string 385 | 386 | for _, a := range c.Services[s].Accounts { 387 | names = append(names, a.AccountName) 388 | } 389 | 390 | namer := func(i int) string { return names[i] } 391 | idx := t.readChoice(namer, len(c.Services[s].Accounts)) 392 | return idx, names[idx] 393 | } 394 | 395 | func userInput(t *term, prompt string, handler func(string) error) { 396 | for { 397 | fmt.Println(prompt) 398 | i := t.readSimpleString() 399 | err := handler(i) 400 | if err != nil { 401 | fmt.Println("Invalid value, ", err.Error()) 402 | continue 403 | } 404 | break 405 | } 406 | } 407 | 408 | //////////////////////////////////////////////////////////////////////////////////////////////////// 409 | //////////////////////////////////////////////////////////////////////////////////////////////////// 410 | 411 | func confirmRename(f actionFunc) actionFunc { 412 | return func(t *term) { 413 | if t.readBoolean("Editing a name will break existing connections. Only do this if you're really ok with fixing everything! Continue with rename? (yes/no)> ") { 414 | f(t) 415 | } else { 416 | fmt.Println("Cancelling name edit.") 417 | } 418 | } 419 | } 420 | 421 | func confirmNewClientCredentials(f actionFunc) actionFunc { 422 | return func(t *term) { 423 | if t.readBoolean("Creating new credentails will break existing connections. Only do this if you're really ok with fixing everything! Continue? (yes/no)> ") { 424 | f(t) 425 | } else { 426 | fmt.Println("Cancelling...") 427 | } 428 | } 429 | } 430 | 431 | //////////////////////////////////////////////////////////////////////////////////////////////////// 432 | //////////////////////////////////////////////////////////////////////////////////////////////////// 433 | 434 | type term struct { 435 | scanner *bufio.Scanner 436 | } 437 | 438 | func newRealTerm() *term { 439 | term := term{} 440 | term.scanner = bufio.NewScanner(os.Stdin) 441 | return &term 442 | } 443 | 444 | func (t *term) readSimpleString() string { 445 | t.scanner.Scan() 446 | return strings.TrimSpace(t.scanner.Text()) 447 | } 448 | 449 | func (t *term) readBoolean(prompt string) bool { 450 | for { 451 | fmt.Printf(prompt) 452 | rawText := t.readSimpleString() 453 | if rawText == "yes" { 454 | return true 455 | } 456 | if rawText == "no" { 457 | return false 458 | } 459 | 460 | fmt.Println("'" + rawText + "' is not 'yes' or 'no'. Please enter a valid option") 461 | } 462 | } 463 | 464 | func (t *term) readChoice(namer func(int) string, length int) int { 465 | for i := 0; i < length; i++ { 466 | fmt.Printf("[%d]: %s\n", i, namer(i)) 467 | } 468 | for { 469 | fmt.Printf("Choose an option> ") 470 | rawText := t.readSimpleString() 471 | i, err := strconv.Atoi(rawText) 472 | if err == nil && i >= 0 && i < length { 473 | return i 474 | } 475 | fmt.Println(rawText + " is not a valid option. Enter only the number of the option.") 476 | } 477 | } 478 | 479 | //////////////////////////////////////////////////////////////////////////////////////////////////// 480 | //////////////////////////////////////////////////////////////////////////////////////////////////// 481 | 482 | type backIs string 483 | 484 | const ( 485 | backIsBack = backIs("Back") 486 | backIsExit = backIs("Exit") 487 | ) 488 | 489 | type action struct { 490 | displayText string 491 | f actionFunc 492 | } 493 | 494 | type actionFunc func(*term) 495 | 496 | func newAction(displayText string, f actionFunc) *action { 497 | return &action{displayText, f} 498 | } 499 | 500 | func takeActionLoop(t *term, backName backIs, actions ...*action) { 501 | keepLoop := true 502 | 503 | back := newAction(string(backName), func(t *term) { keepLoop = false }) 504 | 505 | actions = append([]*action{back}, actions...) 506 | 507 | for keepLoop { 508 | takeAction(t, actions...) 509 | } 510 | } 511 | 512 | func takeAction(t *term, actions ...*action) { 513 | i := t.readChoice(func(j int) string { return actions[j].displayText }, len(actions)) 514 | actions[i].f(t) 515 | } 516 | -------------------------------------------------------------------------------- /testserver/testserver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* 18 | This implements a very minimal test server. It fakes an implementation of 19 | oauth2, so that the setup process can be verified. It also implements some 20 | endpoints that require authentication, so that basic interaction can be tested. 21 | */ 22 | package main 23 | 24 | import ( 25 | "encoding/json" 26 | "flag" 27 | "fmt" 28 | "log" 29 | "net/http" 30 | "net/http/httputil" 31 | ) 32 | 33 | var certFile *string = flag.String( 34 | "certFile", 35 | "/etc/webapigateway/cert/fullchain.pem", 36 | "This is the full public certificate for this web server.", 37 | ) 38 | 39 | var keyFile *string = flag.String( 40 | "keyFile", 41 | "/etc/webapigateway/cert/privkey.pem", 42 | "This is the private key for the certFile.", 43 | ) 44 | 45 | const ClientID = "ID" 46 | const ClientSecret = "SECRET" 47 | const AuthorizationToken = "AuthorizationToken" 48 | const RefreshToken = "RefreshToken2" 49 | const AccessToken = "AccessToken2" 50 | 51 | func main() { 52 | flag.Parse() 53 | 54 | mux := http.NewServeMux() 55 | mux.HandleFunc("/oauth/auth", auth) 56 | mux.HandleFunc("/oauth/token", token) 57 | 58 | mux.Handle("/service/hello", behindAuth(createTestPage("Hello World."))) 59 | mux.Handle("/service/redirect1", behindAuth(createTestRedirect("https://localhost:2157/service/redirect3"))) 60 | 61 | log.Println("Starting server...") 62 | go secondServer() 63 | log.Fatal(http.ListenAndServeTLS(":2156", *certFile, *keyFile, logAllRequests("2156", mux))) 64 | } 65 | 66 | func secondServer() { 67 | mux := http.NewServeMux() 68 | mux.Handle("/service/redirect3", createTestRedirect("https://localhost:2157/service/redirect4")) 69 | mux.Handle("/service/redirect4", createTestPage("I was certainly redirected.")) 70 | 71 | log.Fatal(http.ListenAndServeTLS(":2157", *certFile, *keyFile, logAllRequests("2157", mux))) 72 | } 73 | 74 | func token(w http.ResponseWriter, r *http.Request) { 75 | { 76 | b, err := httputil.DumpRequest(r, true) 77 | fmt.Println(err) 78 | fmt.Printf("%s\n", string(b)) 79 | } 80 | err := r.ParseForm() 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | switch r.FormValue("grant_type") { 86 | case "authorization_code": 87 | assertFormValue(r, "code", AuthorizationToken) 88 | case "refresh_token": 89 | assertFormValue(r, "refresh_token", RefreshToken) 90 | default: 91 | panic(fmt.Sprintf("unknown grant_type: %s", r.FormValue("grant_type"))) 92 | } 93 | w.Header().Set("Content-Type", "text/json") 94 | writeJson(w, map[string]interface{}{ 95 | "access_token": AccessToken, 96 | "refresh_token": RefreshToken, 97 | "token_type": "Bearer", 98 | "expires_in": 3600, 99 | }) 100 | } 101 | 102 | func assertFormValue(r *http.Request, field, expected string) { 103 | if r.FormValue(field) != expected { 104 | panic(fmt.Sprintf("Form value %s expected '%s', but was '%s'", field, expected, r.FormValue(field))) 105 | } 106 | } 107 | 108 | func writeJson(w http.ResponseWriter, v interface{}) { 109 | b, err := json.Marshal(v) 110 | if err != nil { 111 | panic(err) 112 | } 113 | fmt.Println("======") 114 | fmt.Println(string(b)) 115 | fmt.Println("======") 116 | _, err = w.Write(b) 117 | if err != nil { 118 | panic(err) 119 | } 120 | } 121 | 122 | func auth(w http.ResponseWriter, r *http.Request) { 123 | err := r.ParseForm() 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | assertFormValue(r, "client_id", ClientID) 129 | assertFormValue(r, "response_type", "code") 130 | assertFormValue(r, "scope", "testscope") 131 | 132 | uri := r.FormValue("redirect_uri") 133 | state := r.FormValue("state") 134 | 135 | rUri := fmt.Sprintf("%s?state=%s&code=%s", uri, state, AuthorizationToken) 136 | 137 | http.Redirect(w, r, rUri, http.StatusTemporaryRedirect) 138 | } 139 | 140 | func createTestPage(text string) http.Handler { 141 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 142 | log.Println("Printing test page: ", text) 143 | w.Write([]byte(text)) 144 | }) 145 | } 146 | 147 | func createTestRedirect(redirectUrl string) http.Handler { 148 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 149 | log.Println("Redirecting to: ", redirectUrl) 150 | http.Redirect(w, r, redirectUrl, http.StatusTemporaryRedirect) 151 | }) 152 | } 153 | 154 | func behindAuth(h http.Handler) http.Handler { 155 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 156 | authHeader := r.Header.Get("Authorization") 157 | expected := "Bearer " + AccessToken 158 | log.Println("Verifying auth") 159 | if authHeader != expected { 160 | w.WriteHeader(http.StatusUnauthorized) 161 | log.Printf("Auth header expected '%s', but was '%s'\n", expected, authHeader) 162 | return 163 | } 164 | log.Println("Verified.") 165 | h.ServeHTTP(w, r) 166 | }) 167 | } 168 | 169 | func logAllRequests(server string, h http.Handler) http.Handler { 170 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 171 | { 172 | b, err := httputil.DumpRequest(r, true) 173 | log.Printf("============= Incoming Request to %s\nLogging Error: %s\nRequest:\n%s\n", server, err, string(b)) 174 | } 175 | h.ServeHTTP(w, r) 176 | }) 177 | 178 | } 179 | --------------------------------------------------------------------------------