Support reverse proxy with path rewrite (#127)

This commit is contained in:
饺子w 2019-02-14 01:47:48 +08:00 committed by Jannis Mattheis
parent 347f3ce39e
commit ec5b1f8c30
11 changed files with 43 additions and 34 deletions

View File

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gotify/location"
"github.com/gotify/server/auth" "github.com/gotify/server/auth"
"github.com/gotify/server/model" "github.com/gotify/server/model"
"github.com/h2non/filetype" "github.com/h2non/filetype"
@ -70,7 +69,7 @@ func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
app.UserID = auth.GetUserID(ctx) app.UserID = auth.GetUserID(ctx)
app.Internal = false app.Internal = false
a.DB.CreateApplication(&app) a.DB.CreateApplication(&app)
ctx.JSON(200, withAbsoluteURL(ctx, &app)) ctx.JSON(200, withResolvedImage(&app))
} }
} }
@ -102,7 +101,7 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
userID := auth.GetUserID(ctx) userID := auth.GetUserID(ctx)
apps := a.DB.GetApplicationsByUser(userID) apps := a.DB.GetApplicationsByUser(userID)
for _, app := range apps { for _, app := range apps {
withAbsoluteURL(ctx, app) withResolvedImage(app)
} }
ctx.JSON(200, apps) ctx.JSON(200, apps)
} }
@ -210,7 +209,7 @@ func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) {
a.DB.UpdateApplication(app) a.DB.UpdateApplication(app)
ctx.JSON(200, withAbsoluteURL(ctx, app)) ctx.JSON(200, withResolvedImage(app))
} }
} else { } else {
ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id)) ctx.AbortWithError(404, fmt.Errorf("app with id %d doesn't exists", id))
@ -302,13 +301,22 @@ func (a *ApplicationAPI) UploadApplicationImage(ctx *gin.Context) {
app.Image = name + ext app.Image = name + ext
a.DB.UpdateApplication(app) a.DB.UpdateApplication(app)
ctx.JSON(200, withAbsoluteURL(ctx, app)) ctx.JSON(200, withResolvedImage(app))
} else { } else {
ctx.AbortWithError(404, fmt.Errorf("client with id %d doesn't exists", id)) ctx.AbortWithError(404, fmt.Errorf("client with id %d doesn't exists", id))
} }
}) })
} }
func withResolvedImage(app *model.Application) *model.Application {
if app.Image == "" {
app.Image = "static/defaultapp.png"
} else {
app.Image = "image/" + app.Image
}
return app
}
func (a *ApplicationAPI) applicationExists(token string) bool { func (a *ApplicationAPI) applicationExists(token string) bool {
return a.DB.GetApplicationByToken(token) != nil return a.DB.GetApplicationByToken(token) != nil
} }
@ -319,15 +327,3 @@ func exist(path string) bool {
} }
return true return true
} }
func withAbsoluteURL(ctx *gin.Context, app *model.Application) *model.Application {
url := location.Get(ctx)
if app.Image == "" {
url.Path = "static/defaultapp.png"
} else {
url.Path = "image/" + app.Image
}
app.Image = url.String()
return app
}

View File

@ -130,7 +130,7 @@ func (s *ApplicationSuite) Test_CreateApplication_returnsApplicationWithID() {
ID: 1, ID: 1,
Token: firstApplicationToken, Token: firstApplicationToken,
Name: "custom_name", Name: "custom_name",
Image: "http://example.com/static/defaultapp.png", Image: "static/defaultapp.png",
UserID: 5, UserID: 5,
} }
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
@ -162,8 +162,8 @@ func (s *ApplicationSuite) Test_GetApplications() {
s.a.GetApplications(s.ctx) s.a.GetApplications(s.ctx)
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
first.Image = "http://example.com/static/defaultapp.png" first.Image = "static/defaultapp.png"
second.Image = "http://example.com/static/defaultapp.png" second.Image = "static/defaultapp.png"
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder) test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
} }
@ -180,8 +180,8 @@ func (s *ApplicationSuite) Test_GetApplications_WithImage() {
s.a.GetApplications(s.ctx) s.a.GetApplications(s.ctx)
assert.Equal(s.T(), 200, s.recorder.Code) assert.Equal(s.T(), 200, s.recorder.Code)
first.Image = "http://example.com/image/abcd.jpg" first.Image = "image/abcd.jpg"
second.Image = "http://example.com/static/defaultapp.png" second.Image = "static/defaultapp.png"
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder) test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
} }

View File

@ -1804,7 +1804,7 @@
"type": "string", "type": "string",
"x-go-name": "Image", "x-go-name": "Image",
"readOnly": true, "readOnly": true,
"example": "https://example.com/image.jpeg" "example": "image/image.jpeg"
}, },
"internal": { "internal": {
"description": "Whether the application is an internal application. Internal applications should not be deleted.", "description": "Whether the application is an internal application. Internal applications should not be deleted.",

View File

@ -10,11 +10,14 @@ import (
// Serve serves the documentation. // Serve serves the documentation.
func Serve(ctx *gin.Context) { func Serve(ctx *gin.Context) {
url := location.Get(ctx) base := location.Get(ctx).Host
ctx.Writer.WriteString(get(url.Host)) if basePathFromQuery := ctx.Query("base"); basePathFromQuery != "" {
base = basePathFromQuery
}
ctx.Writer.WriteString(get(base))
} }
func get(host string) string { func get(base string) string {
box := packr.NewBox("./") box := packr.NewBox("./")
return strings.Replace(box.String("spec.json"), "localhost", host, 1) return strings.Replace(box.String("spec.json"), "localhost", base, 1)
} }

View File

@ -16,12 +16,13 @@ func TestServe(t *testing.T) {
ctx, _ := gin.CreateTestContext(recorder) ctx, _ := gin.CreateTestContext(recorder)
withURL(ctx, "http", "example.com") withURL(ctx, "http", "example.com")
ctx.Request = httptest.NewRequest("GET", "/swagger", nil) ctx.Request = httptest.NewRequest("GET", "/swagger?base="+url.QueryEscape("127.0.0.1/proxy/"), nil)
Serve(ctx) Serve(ctx)
content := recorder.Body.String() content := recorder.Body.String()
assert.NotEmpty(t, content) assert.NotEmpty(t, content)
assert.Contains(t, content, "127.0.0.1/proxy/")
} }
func withURL(ctx *gin.Context, scheme, host string) { func withURL(ctx *gin.Context, scheme, host string) {

View File

@ -39,10 +39,15 @@ var ui = `
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.20.5/swagger-ui-bundle.js"> </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.20.5/swagger-ui-bundle.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.20.5/swagger-ui-standalone-preset.js"> </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.20.5/swagger-ui-standalone-preset.js"> </script>
<script> <script>
function getBaseURL() {
var path = window.location.pathname
path = path.substr(0, path.lastIndexOf('/')+1)
return window.location.host + path;
}
window.onload = function() { window.onload = function() {
// Begin Swagger UI call region // Begin Swagger UI call region
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: "../swagger", url: "swagger?base="+encodeURIComponent(getBaseURL()),
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ presets: [

View File

@ -39,7 +39,7 @@ type Application struct {
// //
// read only: true // read only: true
// required: true // required: true
// example: https://example.com/image.jpeg // example: image/image.jpeg
Image string `json:"image"` Image string `json:"image"`
Messages []MessageExternal `json:"-"` Messages []MessageExternal `json:"-"`
} }

View File

@ -2,6 +2,7 @@
"name": "gotify-ui", "name": "gotify-ui",
"version": "0.2.0", "version": "0.2.0",
"private": true, "private": true,
"homepage": ".",
"dependencies": { "dependencies": {
"@material-ui/core": "^1.5.1", "@material-ui/core": "^1.5.1",
"@material-ui/icons": "^2.0.3", "@material-ui/icons": "^2.0.3",

View File

@ -18,6 +18,7 @@ import AddApplicationDialog from './AddApplicationDialog';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {observable} from 'mobx'; import {observable} from 'mobx';
import {inject, Stores} from '../inject'; import {inject, Stores} from '../inject';
import * as config from '../config';
import UpdateDialog from './UpdateApplicationDialog'; import UpdateDialog from './UpdateApplicationDialog';
@observer @observer
@ -152,7 +153,7 @@ const Row: SFC<IRowProps> = observer(
<TableRow> <TableRow>
<TableCell padding="checkbox"> <TableCell padding="checkbox">
<div style={{display: 'flex'}}> <div style={{display: 'flex'}}>
<Avatar src={image} /> <Avatar src={config.get('url') + image} />
<IconButton onClick={fUpload} style={{height: 40}}> <IconButton onClick={fUpload} style={{height: 40}}>
<CloudUpload /> <CloudUpload />
</IconButton> </IconButton>

View File

@ -21,9 +21,10 @@ const defaultDevConfig = {
url: 'http://localhost:80/', url: 'http://localhost:80/',
}; };
const {port, hostname, protocol} = window.location; const {port, hostname, protocol, pathname} = window.location;
const slashes = protocol.concat('//'); const slashes = protocol.concat('//');
const url = slashes.concat(port ? hostname.concat(':', port) : hostname); const path = pathname.endsWith('/') ? pathname : pathname.substring(0, pathname.lastIndexOf('/'));
const url = slashes.concat(port ? hostname.concat(':', port) : hostname) + path;
const urlWithSlash = url.endsWith('/') ? url : url.concat('/'); const urlWithSlash = url.endsWith('/') ? url : url.concat('/');
const defaultProdConfig = { const defaultProdConfig = {

View File

@ -5,6 +5,7 @@ import Delete from '@material-ui/icons/Delete';
import React from 'react'; import React from 'react';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
import Container from '../common/Container'; import Container from '../common/Container';
import * as config from '../config';
import {StyleRulesCallback} from '@material-ui/core/styles/withStyles'; import {StyleRulesCallback} from '@material-ui/core/styles/withStyles';
const styles: StyleRulesCallback = () => ({ const styles: StyleRulesCallback = () => ({
@ -57,7 +58,7 @@ class Message extends React.PureComponent<IProps & WithStyles<typeof styles>> {
<Container style={{display: 'flex'}}> <Container style={{display: 'flex'}}>
<div className={classes.imageWrapper}> <div className={classes.imageWrapper}>
<img <img
src={image} src={config.get('url') + image}
alt="app logo" alt="app logo"
width="70" width="70"
height="70" height="70"