├── 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 |
28 |
29 |
32 | {{if .Profile}}
33 |
38 |
41 |
42 | {{if .Profile.ImageURL}}
43 |
44 | {{end}}
45 |
{{.Profile.DisplayName}}
46 |
47 |
48 |
49 |
50 | {{if .Flash}}
51 |
52 | {{.Flash}}
53 |
54 | {{end}}
55 | {{template "body" .Data}}
56 |
57 | {{else}}
58 |
61 |
62 |
63 |
64 | {{if .Flash}}
65 |
66 | {{.Flash}}
67 |
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 |
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 |
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 |
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 |
31 |
32 | {{else}}
33 |
34 |
35 | Services/Accounts
36 | Actions
37 |
38 | {{end}}
39 | {{range .Services}}
40 | {{$ServiceName := .ServiceName}}
41 |
42 | {{$ServiceName}}
43 |
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 | Cancel
59 |
60 |
61 | Remove
62 |
63 |
64 |
65 | {{range .Accounts}}
66 |
67 | {{.AccountName}}
68 |
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 | Cancel
83 |
84 |
85 | Remove
86 |
87 |
89 | Get Account Key
90 |
91 |
93 | Reauthorize
94 |
95 |
96 |
97 |
98 |
106 | {{end}}
107 |
115 | {{end}}
116 |
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 |
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 |
27 |
28 |
29 |
30 | Emails
31 | Actions
32 |
33 | {{range $key, $v := .}}
34 |
35 |
36 | {{$key}}
37 |
38 |
39 |
40 | Remove user
41 | Please review and confirm your request.
42 | Remove {{$key}}
43 |
44 | Save
45 | Cancel
46 |
47 | Remove
48 |
49 |
50 |
58 | {{end}}
59 |
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 |
--------------------------------------------------------------------------------