레이블이 computer인 게시물을 표시합니다. 모든 게시물 표시
레이블이 computer인 게시물을 표시합니다. 모든 게시물 표시

[컴] web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

cors 설정 / redirect fetch / fetch cors

web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

  • 다음 sequence diagram 은 만약 내가 site.a.com 에 접속한 상황에서 fetch 로 api.a.com 에 request 를 했다. 그 상황에서 api.a.com 가 redirect site.a.com 를 나에게 던져준 경우이다.
  • 이 경우 예상치 못하게 cors error 가 떴다.

CORS error 가 뜨는 이유

CORS error 가 뜨는 이유는 Origin 이 null 로 set 돼서 site.a.com 로 request 를 보내기 때문이다.

  • 여기를 보면, 처음 request 를 보낸 url에서 바뀐적이 있으면, redirect-tainted origin 으로 본다. 그렇기에 Origin 이 null 로 잡힌다.
  • 이러면 origin 이 ‘null’ 로 바뀌었다고 봐야 하기 때문에 site.a.com 에서 CORS 를 지원하기 위한 설정을 잡아줘야 한다.

다음처럼 api.a.com 으로 보내고 api.a.com 으로 redirect 되는 경우는 다음처럼 CORS error 가 안뜨고, 잘 동작한다.

api.a.com, api.a.com

CORS error 가 안뜨게 수정

  • 아래는 이때 CORS error 가 뜨지 않도록 header 를 맞춰준 예시이다.

조정한 내용:

  • Accee-Control-Allow-Origin: null

  • Access-Control-Allow-Credentials: true

  • Access-Control-Allow-HeadersAccess-Control-Request-Headers 에서 요청한 header를 넣어준다.

  • Access-Control-Allow-MethodsAccess-Control-Request-Method에 맞춰서 지정

  • api.a.com 으로 보내고 site.a.com 으로 redirect 되는 경우 : mermaid link

api.a.com, site.a.com

fetch 를 사용할 때 redirect :

CORS error 가 안뜨게 하려면

CORS error 가 안뜨게 하려면, 다음처럼 설정을 하면 된다.

client :

  • `credentials : ‘omit’
  • mode : 'no-cors'

server :

  • Access-Control-Allowed-Origin: *
  • Access-Control-Allow-Credentials: false

Fetch: Cross-Origin Requests

  • client 에서 Origin을 보내면, 그것을 server가 본다.
  • server 는 그에 따라 Access-Control-Allow-Origin 을 만들어서 response 에 보낸다.
  • browser 가 trusted mediator(신뢰할 수 있는 중재자) 역할을 한다. 즉, browser가 이제 client 에서 보냈던 origin 이랑 서버가 보낸 Access-Control-Allow-Origin 값을 비교해서 값이 적절하면, javascript 가 response 값을 access 할 수 있게 해준다.

javascript 가 기본적으로 접근할 수 있는 safe header 는 다음과 같다.

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

js 가 다른 header 에 접근하고 싶어도 browser가 접근을 막는다. 다만 server 에서 Access-Control-Expose-Headers 에 추가된 header는 browser가 js 한테 열어준다.

이제는 browser가 안전하지 않은 요청(unsafe request)는 바로 날리지 않는다. preflight request 를 먼저 날리고 나서 날리게 된다.

  • Access-Control-Request-Method : 어떤 method 로 보내려는지 적는다.
  • Access-Control-Request-Headers : 보내려는 request 가 갖고 있는 header들을 적는다.
  • Origin : request 가 어디서 보내는 request 인지를 적게 된다.
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
Content-Encoding: gzip
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Encoding,API-Key

서버단에서 redirect 시켜서 CORS 피하기

  • preflight 등의 성능을 방해하는 요소를 줄일 수 있다.
                              ┌─►  http://localhost:8081       
   http://localhost:8080      │     ┌─────────────┐            
                              │     │             │            
   http://localhost:8080/api──┘     │   WAS       │            
     ┌─────────────┐                │             │            
     │ web server  │                │             │            
     │     ┌───────┤                │             │            
     │     │       │                └─────────────┘            
     │     │ asset │                                           
     │     │       │                                           
     └─────┴───────┘                                           

[컴][웹] Http Header Origin 이 null 인 경우

spec , origin이 널 / null 인 경우 / why origin null

Http Header Origin 이 null 인 경우

origin 값은 opaque origin 또는 tuple origin 인데, tuple origin 이 정상적인 값으로 ‘http://daum.net’ 같은 값이고, opaque origin (이해하기 힘든 origin)은 null 이라고 보면 된다.

The serialization of ("https", "xn--maraa-rta.example", null, null) is "https://xn--maraa-rta.example".

아래 링크에서 origin 이 null 이 되는 모든 case 를 이야기 해준다.

fetch 를 A에 했는데, B 로 redirect 되는 경우 Origin 은 null

fetch 를 한 경우 다른 origin 으로 redirect 하면 null 이 된다.

See Also

  1. 쿠…sal: [컴] web 에서 B site 에서 A server 로 fetch 를 보내고, B로 redirect 를 받은 경우

[컴] 쿠팡, 판매상품의 재고가 0으로 표시되던 현상, redis 이슈

 에러 / 장애 / 캐시 / 레디스 이슈 / 레디스 버그 /

쿠팡, 판매상품의 재고가 0으로 표시되던 현상, redis 이슈

장애

  • 2019년 7월 24일 오전 7시경부터 쿠팡 판매 상품의 재고가‘0’으로 표시돼, 소비자는 관련 상품의 주문 및 구매할 수 없었다.

장애원인

쿠팡에서 직접적으로 밝히진 않았지만, 장애원인으로 지목되는 것은 다음 이슈이다.

간략하게 이야기하면 item 을 추가할 때 쓰는 dictAddRaw 함수의 버그이다. long long(64bit) 으로 사용해야 할 index를 int 로 사용해서 문제가 발생했다.

Reference

  1. 쿠팡 오류 원인은 오픈소스 ‘레디스 DB’ 때문 < 게임·인터넷 < 기사본문 - 디지털투데이 (DigitalToday), 2019-07-24
  2. 쿠팡 오류 원인은 오픈소스 ‘레디스 DB’ 때문 : 클리앙

[컴] Spring 의 securityWebFilterChain 에서 filter 가 추가되는 과정

스프링 / springboot / 스프링부트 / security

Spring 의 securityWebFilterChain 에서 filter 가 추가되는 과정

  • WebFilterChainProxy.filter()에서 WebFluxSecurityConfiguration.securityWebFilterChains 을 돌면서 실행한다.
  • WebFilterChainProxy@Bean WebFluxSecurityConfiguration.springSecurityWebFilterChainFilter 에 의해서 new 된다.
  • securityWebFilterChains@Autowire setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) 에 의해 new 된다.
  • WebFluxSecurityConfiguration@EnableWebFluxSecurity 에 의해 불려진다.
  • @EnableWebFluxSecuritySecurityConfig 에 설정돼 있다.
  • @Bean SecurityConfig.securityWebFilterChain 에서 filter를 정하고, ServerHttpSecurity.build() 를 호출하는데, 이 때 filter들이 실제로 추가된다.
    • formLogin 인 경우 login url 인 ‘/login’ 도 설정된다.

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig
    @Bean
    SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            ...
            .authorizeExchange(exchange -> exchange
                    ...
                    .and()
                    .formLogin(formLogin -> formLogin
                            .authenticationSuccessHandler(authenticationSuccessHandler)
                            .authenticationFailureHandler(authenticationFailureHandler)
                    )
            );
        return http.build();    // ServerHttpSecurity.build
    }

public class ServerHttpSecurity
    private FormLoginSpec formLogin;
    public SecurityWebFilterChain build() {
        if (this.formLogin != null) {
            if (this.formLogin.authenticationManager == null) {
                this.formLogin.authenticationManager(this.authenticationManager);
            }
            if (this.formLogin.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.formLogin.securityContextRepository);
            }
            else if (this.securityContextRepository != null) {
                this.formLogin.securityContextRepository(this.securityContextRepository);
            }
            else {
                this.formLogin.securityContextRepository(new WebSessionServerSecurityContextRepository());
            }
            // add filter to the this.webFilters
            this.formLogin.configure(this);
        }
        ...
        AnnotationAwareOrderComparator.sort(this.webFilters);
        List<WebFilter> sortedWebFilters = new ArrayList<>();
        this.webFilters.forEach((f) -> {
            if (f instanceof OrderedWebFilter) {
                f = ((OrderedWebFilter) f).webFilter;
            }
            sortedWebFilters.add(f);
        });
        sortedWebFilters.add(0, new ServerWebExchangeReactorContextWebFilter());

        return new MatcherSecurityWebFilterChain(getSecurityMatcher(), sortedWebFilters);
    }

    public final class FormLoginSpec {
        protected void configure(ServerHttpSecurity http) {
            if (this.authenticationEntryPoint == null) {
                this.isEntryPointExplicit = false;
                loginPage("/login");
            }
            ...
        }
    }


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ ServerHttpSecurityConfiguration.class, WebFluxSecurityConfiguration.class,
        ReactiveOAuth2ClientImportSelector.class })
@Configuration
public @interface EnableWebFluxSecurity {

}

@Configuration(proxyBeanMethods = false)
class WebFluxSecurityConfiguration {

    private List<SecurityWebFilterChain> securityWebFilterChains;

    ...

    @Autowired(required = false)
    void setSecurityWebFilterChains(List<SecurityWebFilterChain> securityWebFilterChains) {
        // SecurityConfig.securityWebFilterChain()
        this.securityWebFilterChains = securityWebFilterChains;
    }

    @Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
    @Order(WEB_FILTER_CHAIN_FILTER_ORDER)
    WebFilterChainProxy springSecurityWebFilterChainFilter() {
        return new WebFilterChainProxy(getSecurityWebFilterChains());
    }

    private List<SecurityWebFilterChain> getSecurityWebFilterChains() {
        List<SecurityWebFilterChain> result = this.securityWebFilterChains;
        if (ObjectUtils.isEmpty(result)) {
            return Arrays.asList(springSecurityFilterChain());
        }
        return result;
    }


public class WebFilterChainProxy implements WebFilter {
    private final List<SecurityWebFilterChain> filters;

    public WebFilterChainProxy(List<SecurityWebFilterChain> filters) {
        this.filters = filters; // WebFluxSecurityConfiguration.securityWebFilterChains
    }
    ...
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return Flux.fromIterable(this.filters)
                .filterWhen((securityWebFilterChain) -> securityWebFilterChain.matches(exchange)).next()
                .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
                .flatMap((securityWebFilterChain) -> securityWebFilterChain.getWebFilters().collectList())
                .map((filters) -> new FilteringWebHandler(chain::filter, filters)).map(DefaultWebFilterChain::new)
                .flatMap((securedChain) -> securedChain.filter(exchange));  // securedChain is `DefaultWebFilterChain`
    }
    ...
}
@AutoConfiguration(after = { WebFluxAutoConfiguration.class })
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnMissingBean(HttpHandler.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class HttpHandlerAutoConfiguration {
    @Configuration(proxyBeanMethods = false)
    public static class AnnotationConfig {

        ...

        @Bean
        public HttpHandler httpHandler(ObjectProvider<WebFluxProperties> propsProvider) {
            HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build();
            ...
            return httpHandler;
        }

    }

}

public final class WebHttpHandlerBuilder{
    public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
        ...
        // set this.filters
        List<WebFilter> webFilters = context
                .getBeanProvider(WebFilter.class)
                .orderedStream()
                .collect(Collectors.toList());
        builder.filters(filters -> filters.addAll(webFilters));
    }
    public HttpHandler build() {
        WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
        decorated = new ExceptionHandlingWebHandler(decorated,  this.exceptionHandlers);

        HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
        ...
        // return adapted
        return (this.httpHandlerDecorator != null ? this.httpHandlerDecorator.apply(adapted) : adapted);
    }
}

public class FilteringWebHandler extends WebHandlerDecorator 
    public FilteringWebHandler(WebHandler handler, List<WebFilter> filters) {
        super(handler);
        this.chain = new DefaultWebFilterChain(handler, filters);
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return this.chain.filter(exchange);
    }

public class DefaultWebFilterChain implements WebFilterChain
    public DefaultWebFilterChain(WebHandler handler, List<WebFilter> filters) {
        Assert.notNull(handler, "WebHandler is required");
        this.allFilters = Collections.unmodifiableList(filters);
        this.handler = handler;
        DefaultWebFilterChain chain = initChain(filters, handler);
        this.currentFilter = chain.currentFilter;
        this.chain = chain.chain;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange) {
        return Mono.defer(() ->
                this.currentFilter != null && this.chain != null ?
                        invokeFilter(this.currentFilter, this.chain, exchange) :
                        this.handler.handle(exchange));
    }
    private Mono<Void> invokeFilter(WebFilter current, DefaultWebFilterChain chain, ServerWebExchange exchange) {
        String currentName = current.getClass().getName();
        return current.filter(exchange, chain).checkpoint(currentName + " [DefaultWebFilterChain]");
    }

requst 를 처리시점에 호출되는 filter()

  1. HttpWebHandlerAdapter에서 getDelegate().handle(exchange)
  2. ExceptionHandlingWebHandler.handle() 에서 super.handle() 을 호출
  3. FilteringWebHandler.handle()을 호출하게 된다.
  4. FilteringWebHandler.handle()에서 DefaultWebFilterChain.filter(exchange) 를 호출
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler 
    @Override
    public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
        ...
        ServerWebExchange exchange = createExchange(request, response);
        ...
        return getDelegate().handle(exchange)
                .doOnSuccess(aVoid -> logResponse(exchange))
                .onErrorResume(ex -> handleUnresolvedError(exchange, ex))
                .then(Mono.defer(response::setComplete));
    }
public class ExceptionHandlingWebHandler extends WebHandlerDecorator 
    ...
    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        Mono<Void> completion;
        try {
            // WebHandlerDecorator.handle --> FilteringWebHandler.handle
            completion = super.handle(exchange);
        }
        catch (Throwable ex) {
            ...
        }

        for (WebExceptionHandler handler : this.exceptionHandlers) {
            completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
        }
        return completion;
    }

public class WebHandlerDecorator implements WebHandler

    private final WebHandler delegate;

    public WebHandlerDecorator(WebHandler delegate) {
        ...
        this.delegate = delegate;
    }
    ...

    @Override
    public Mono<Void> handle(ServerWebExchange exchange) {
        return this.delegate.handle(exchange);
    }

/login 호출

curl http://localhost:8888/login
  • DefaultWebFilterChain.filter
  • –> AuthenticationWebFilter.filter
  • --> AuthenticationWebFilter.authenticate()
  • --> authenticationManager.authenticate(token)
  • --> AbstractUserDetailsReactiveAuthenticationManager.authenticate

[컴] ChatGPT 에서 다른 사용자의 개인정보와 채팅제목이 노출되던 이슈, 2023-03-20

openAI 이슈 / 챗gpt /

ChatGPT 에서 다른 사용자의 개인정보와 채팅제목이 노출되던 이슈, 2023-03-20

2023년 3월 20일, chatgpt에서 특정사용자가 채팅기록 사이드바에서 다른 사용자의 대화에 대한 간략한 설명을 볼 수 있었다.(ref. 3)

redis-py library 의 이슈로 다른 사용자의 개인 정보와 채팅 제목이 노출되었다고 한다.

redis-py library 의 이슈

github issue 에 가면, 아래와 같은 재현 code를 확인할 수 있다.

import asyncio
from redis.asyncio import Redis
import sys


async def main():
    myhost, mypassword = sys.argv[1:]
    async with Redis(host=myhost, password=mypassword, ssl=True, single_connection_client=True) as r:

        await r.set('foo', 'foo')
        await r.set('bar', 'bar')

        t = asyncio.create_task(r.get('foo'))
        await asyncio.sleep(0.001)
        t.cancel()
        try:
            await t
            print('try again, we did not cancel the task in time')
        except asyncio.CancelledError:
            print('managed to cancel the task, connection is left open with unread response')

        print('bar:', await r.get('bar'))
        print('ping:', await r.ping())
        print('foo:', await r.get('foo'))


if __name__ == '__main__':
    asyncio.run(main())
$ python redis_cancel.py hostname password
managed to cancel the task, connection is left open with unread response
bar: b'foo'
ping: False
foo: b'PONG'

대략 그림으로 설명하면 이런 모습이다.

redis-py library 의 comuunicatino queue

위 그림에서 ’파란색’부분이 t에서 사용했던 부분인데, 이것이 connection 이 cancel 됐을때도 계속 남아있어서, 그 다음 command 인 r.get('bar') 를 send 할 때, r.get('foo') 가 대신 보내지는 것인 듯 하다. 그래서 결과가 하나씩 밀려서 나온다.

Reference

  1. OpenAI Reveals Redis Bug Behind ChatGPT User Data Exposure Incident, 2023-03-25
  2. Off by 1 - Canceling async Redis command leaves connection open, in unsafe state for future commands · Issue #2624 · redis/redis-py · GitHub
  3. ChatGPT Users Report Being Able to See Random People’s Chat Histories

[컴] node v20.x 에서 TLS v1.2 사용

node v20.x 에서 TLS v1.2 사용

댓글fetch, axios 에선 TLS v1.2 를 사용할 수 없다고 한다.

axios 는 모르겠지만, fetch 에선 TLS v1.2 로 통신하지 못했다. 그래서 https 를 사용해서 해결했다.

node, SSL_OP_LEGACY_SERVER_CONNECT

https.Agent

axios 나 fetch 나 이방법으로 해결이 가능하다.

const httpsAgent = new https.Agent({
  // for self signed you could also add
  // rejectUnauthorized: false,
  
  // allow legacy server
  secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
});
const response = await fetch(url, {
  agent: httpsAgent,
});

const html = await response.text();
console.log(html)

실패: secureOptions 없이 node-fetch 에서 https.Agent 를 사용

이 방법은 적어도 windows 에서는 동작하지 않았다.

import https from "https";
const agent = new https.Agent({
  rejectUnauthorized: false
});
fetch(myUrl, { agent });

아래와 같은 error 가 떴다.

reason: write EPROTO 382D0000:error:0A000152:SSL routines:final_renegotiate:unsafe legacy renegotiation disabled:c:\ws\deps\openssl\openssl\ssl\statem\extensions.c:922:

    at ClientRequest.<anonymous> (D:\a\prog\typescript\nextjs\budgetbuddy\budgetbuddybrain\node_modules\node-fetch\lib\index.js:1501:11)
    at ClientRequest.emit (node:events:518:28)
    at TLSSocket.socketErrorListener (node:_http_client:500:9)
    at TLSSocket.emit (node:events:518:28)
    at emitErrorNT (node:internal/streams/destroy:169:8)
    at emitErrorCloseNT (node:internal/streams/destroy:128:3)
    at processTicksAndRejections (node:internal/process/task_queues:82:21) {
  type: 'system',
  errno: 'EPROTO',
  code: 'EPROTO'
}
...

실패: https.request

처음에는 req.on('data', (chunk) => {}) 부분을 실행하지 않았어서 잘 동작하는 줄 알았는데, 아니었다. 이 방법도 에러가 나온다.

const options = {
  hostname: 'www.howsmyssl.com',
  port: 443,
  path: '/a/check',
  method: 'GET',
  secureProtocol: "TLSv1_2_method"
}
const req = await https.request(options)
let responseBody = '';
req.on('data', (chunk) => {
  console.log(chunk)
  responseBody += chunk;
});
req.on('end', () => {
  console.log('end')
  console.log(`BODY: ${responseBody}`);
});
req.on('error', function(e) {
  console.log('problem with request: ' + e.message);
});

Reference

  1. node.js - Configure https agent to allow only TLS1.2 for outgoing requests - Stack Overflow

[컴] NestJS 설치 및 간단한 api 생성

orm 사용 / nestjs 에서 orm 사용 / nestjs example / nestjs simple api / 사용법

NestJS 설치 및 간단한 api 생성

@nestjs/cli를 사용해서 boiler plate 를 만들 수 있다.

npm i -g @nestjs/cli

원하는 project 이름으로 만들어보자. 여기서는 myapp 으로 했다.

nest new myapp

dev server

package.json 을 참고하면 된다.

npm run start:dev

main.ts 를 보면 알겠지만, 기본 port 는 3000 으로 되어 있다. 브라우저에서 http://localhost:3000 로 접속하자. Hello World 가 보이면 된다.

config

config 파일을 사용하려면 @nestjs/config를 설치해야 한다.

npm i --save @nestjs/config

module 추가

각 기능별로 Module 을 만들어서 추가하면 된다. Application 에는 하나의 root module 이 있고, 이 root module 이 모든 module 을 import 하게 하면 된다. root module 은 nest product 를 new 할 때 만들어진다. src/app.module.ts

여기서는 MyFuncModule 을 만들어보자. Module 은 어떤 controller 를 사용하고, 어떤 provider 를 사용하는지 명시하게 된다.

controller 에서 'myfunc' 을 만들어주면 http://localhost:3000/myfunc/ 를 호출할 수 있다.

src/
  |
  + app.module.ts
  + app.controller.ts
  + app.service.ts
  |
  +-- myfunc/
        |
        + myfunc.controller.ts
        + myfunc.module.ts
        + myfunc.service.ts

AppModule :

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyFuncModule } from './diper/myfunc.module';

@Module({
  imports: [MyFuncModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

MyFuncModule :

import { Module } from '@nestjs/common';
import { MyFuncController } from './myfunc.controller';
import { MyFuncService } from './myfunc.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MyFunEntity } from 'src/model/myfunc.entity';

@Module({
  imports: [TypeOrmModule.forFeature([MyFuncEntity]],
  controllers: [MyFuncController],
  providers: [MyFuncService],
})
export class MyFuncModule {}
import { Controller, Get } from '@nestjs/common';
import { MyFuncService } from './myfunc.service';
import { MyFuncEntity } from 'src/model/myfunc.entity';

@Controller('myfunc')
export class MyFuncController {
  constructor(private readonly myFunc: MyFuncService) {}

  @Get()
  async findAll(): Promise<MyFuncEntity[]> {
    return await this.myFunc.findAll();
  }
}
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MyFuncEntity } from 'src/model/myfunc.entity';
import { Repository } from 'typeorm';

@Injectable()
export class MyFuncService {
  constructor(
    @InjectRepository(MyFuncEntity)
    private readonly myFuncEntityRepository: Repository<MyFuncEntity>,
  ) {}

  async findAll(): Promise<MyFuncEntity[]> {
    return await this.diperRepository.find();
  }
  
  getHello(): string {
    return 'Hello World!';
  }
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class MyFuncEntity {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    price: number

    @Column()
    date: Date

}

Database ORM

기본으로 TypeORM 을 이용한 TypeORMModule 을 제공한다. module 은 설치해야 한다. 다음은 mysql 과 관련한 설치 command 이다. 다른 db 에 대한 정보는 TypeORM - Installation 를 참고하자.

npm install --save @nestjs/typeorm typeorm mysql2

설치를 하고 나서 TypeOrmModule 을 root module 에 import 하면 된다.

  • entites option: 아래 code 에서 entities 에 만들 table 의 정보가 들어가면 된다. 저곳에 명시하지 않으면 해당 table 을 만들지 않는다.
  • autoLoadEntities : 자동으로 entity 를 불러온다. 기본값은 false 이다.
  • synchronize : 이 값은 기본값이 true이다. 이 값을 true로 하면, entity 가 변경내역이 서버 시작 시점에 반영된다. production 에선 이 값을 false 로 두라고 한다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test', 
      entities: [],     // <---- info for tables
      synchronize: true // <---- 
    }),
  ],
})
export class AppModule {}

DataSource 를 사용하는 법

아래처럼 ConfigModule.forRoot 에서 config file 을 불러올지 정해주고, 그값들을 TypeOrmModule.forRootAsync에서 inject 해서 사용하면 된다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MyFuncModule } from './diper/myfunc.module';

import { ConfigModule, ConfigService, } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: ['.env.dev.local', '.env.dev'],
      isGlobal: true
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return ({
          type: 'mysql',
          host: configService.get('HOST'),
          port: +configService.get('PORT'),
          username: configService.get('DATABASE_USER'),
          password: configService.get('DATABASE_PASSWORD'),
          database: configService.get('DATABASE'),
          // entities: [
          //   __dirname + '/**/*.entity{.ts,.js}',
          // ],
          autoLoadEntities: true,
          synchronize: true,
        })
      }
    }),
    MyFuncModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

TypeOrmModule.forRootAsync

TypeOrmModule.forRootAsync 를 사용하면, 무조건 entity 에 대한 table 을 생성하지 않는다. 각 module 에서 자신들이 사용할 entity 를 TypeOrmModule.forFeature([MyFuncEntity]) 처럼 imports에 적어주게 되는데, 이렇게 적힌 것들에 대해서만 생성해 준다.

data migration

최초의 table 의 생성은 ConfigModule.forRoot 의 option entities를 통해 정보를 가져와서 생성하게 된다.

NestJS 에서 제공하진 않고, TypeORM CLI 를 이용한다.(참고) typeorm package 가 이미 설치되어 있으니, 이걸 이용하면 된다.

migration:create :

아래처럼 하면, ./migration/test01.ts 가 생성된다. 그러면 이 test01.ts에 migration 내용을 적으면 된다.

.\node_modules\.bin\typeorm migration:create ./migration/test01

migration:run :

.ts file 을 js 로 바꿔서 사용하기 위해 ts-node 를 사용해야 한다. 다음 커맨드를 이용하면 된다.

npx typeorm-ts-node-commonjs migration:run -- -d path-to-datasource-config

full codes

Refrence

  1. First steps | NestJS - A progressive Node.js framework

[컴] NestJS 에서 ConfigModule 예시

nestjs config / env / 설정

NestJS 에서 ConfigModule 예시

NestJS 에서는 configuration 을 set할 때 ConfigService를 노출하는 ConfigModule 을 사용하는 것을 권장한다. 이 ConfigService가 알맞은 .env file 을 load 하는 것이다.

직접 만들어도 되고, @nestjs/config package 를 사용해도 된다. @nestjs/config package 내부에서 dotenv 를 사용한다.

npm i --save @nestjs/config

root module 에 아래처럼 ConfigModule을 추가한다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

import { ConfigModule,ConfigService } from '@nestjs/config';

@Module({
    // 이 때 1번째로 import 를 해야 그 이후 module 에서 ConfigModule을
    // 접근할 수 있다.
  imports: [ConfigModule.forRoot({
      // .env.dev.local 을 찾고, 없으면 .env.dev 를 찾는다.
      // 기본적으로 `.env`를 찾는다.
      envFilePath: ['.env.dev.local', '.env.dev'],
      // true 이면, global module 이 된다. 그래서 root module에서 한번만 import 하면, 
      // 다른 module 에서 import 없이 사용할 수 있다.
      isGlobal: true
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => {
        return ({
          type: 'mysql',
          host: configService.get('HOST'),
          port: +configService.get('PORT'),
          username: configService.get('DATABASE_USER'),
          password: configService.get('DATABASE_PASSWORD'),
          database: configService.get('DATABASE'),
          // entities: [
          //   __dirname + '/**/*.entity{.ts,.js}',
          // ],
          autoLoadEntities: true,
          synchronize: true,
        })
    }}),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

.env file :

DATABASE_USER=test
DATABASE_PASSWORD=test

이제 .env file 의 변수가 load 되면, nestjs 에서 process.env.DATABASE_USER 등을 이용해서 사용할 수 있게 된다. 그리고 또한 ConfigService 에도 저장돼서 ConfigService.get() 를 통해서도 접근할 수 있게 된다. ConfigService provider는 ConfigModule.forRoot()를 통해 등록돼서 사용할 수 있게 된다.

즉 process.env 에 .env file 의 값들을 load 한 것이라 볼 수 있다.

dist directory에 복사

nest build 시에는 .ts file 외에는 자동으로 복사하지 않는다. 그래서 nest-cli.jsoncompilerOptions#assets 에 아래처럼 복사가 되도록 설정을 해놔야 한다.

{
  ...
  "compilerOptions": {
    "assets": [{"include": "../config/*.yaml", "outDir": "./dist/config"}]
  }
}

기타

yaml file, 환경변수 사용등의 자세한 이야기는 ref. 1 을 참고하자.

Reference

  1. Configuration | NestJS - A progressive Node.js framework
  2. Ultimate Guide: NestJS Config & Environment Variables [2022]

[컴][머신러닝] windows 에서 ollama 사용

 

llama / llm / 윈도우즈 /

windows 에서 ollama 사용

다음 링크에서 Ollama windows version 을 download 하자.

설치를 하면 %AppData%\Local\Programs\Ollama에 자동으로 설치된다. 대략 1.6GB 정도 여유공간이 필요하다.

  • %AppData%\Local\Programs\Ollama

아래와 같은 방식으로 실행하면 된다. model 이 없으면 model 을 download 하고, 실행한다. ollama run llama2 를 하면 llama2 model 을 다운로드하게 된다. 대략 3.8GB 이다. 이 model 은 %UserProfile%\.ollama 에 저장된다.

ollama run llama2
>>> Send a message (/? for help)
ollama list

Ollama WebUI

Ollama WebUI 가 있다. 아래는 linux 에서 설치방법이다.

sudo docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
sudo docker run -d --network=host -v ollama-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://172.17.192.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main

[컴] systemctl --user도 linger 설정이 필요하다.

스프링 이유없이 죽는 이유

systemctl --user도 linger 설정이 필요하다.

systemctl --user 를 사용하는 경우에도 linger 설정이 필요하다.

만약 ssh login 을 해서 서버를 띄운 후에 ssh세션을 모두 닫는다면 그 시점에 서버가 종료될 것이다.

sudo loginctl enable-linger <user_id>

현재 linger설정 상태를 확인하려면 아래와 같은 command를 치면 된다.

loginctl user-status <user_id>

$ loginctl user-status admin
admin (1000)
           Since: Thu 2024-03-28 06:16:00 UTC; 4h 42min ago
           State: active
        Sessions: 642 *639
          Linger: no
            Unit: user-1000.slice
                  ├─session-639.scope
                  │ ├─67433 sshd: admin [priv]
                  │ ├─67456 sshd: admin@pts/0
                  │ ├─67457 -bash
                  │ ├─67906 loginctl user-status admin
                  │ └─67907 pager
                  ├─session-642.scope
                  │ ├─67469 sshd: admin [priv]
                  │ ├─67476 sshd: admin@notty
                  │ └─67477 /usr/lib/openssh/sftp-server
                  └─user@1000.service
                    ├─app.slice
                    │ └─myserver.service
                    │   └─67481 /usr/bin/java -Dspring.profiles.active=prod -Xms512M -Xmx1G -XX:+UseZGC -jar /services/myserver/myserver-0.0.1-SNAPSHOT.jar
                    └─init.scope
                      ├─67436 /lib/systemd/systemd --user
                      └─67437 (sd-pam)

개인적인 경험

처음에 systemctl --user 를 사용할 떄는 이것이 systemctl 과 같다고 봐서, daemon 으로 동작하기에 따로 nohup 등의 설정이 필요없다고 생각했다. 그래서 systemctl --user를 이용해서 웹서버를 실행했었다.

하지만 이 서버가 종종 gracefully shutdown 으로 죽었다.(spring server 였다.) 당연히 웹서버(WAS) 로그에는 남아있는 것이 없었다.

원인을 찾다가 deamon.log를 보게 되었다. 여기서 systemd가 stop 된 것을 보고 웹서버(WAS)가 죽은 이유를 추측해 볼 수 있었다. 그래서 결국 linger 설정이 필요한 것을 알았다.

cat /var/log/daemon.log | grep "Mar 28 06:12"

...
Mar 28 06:12:45 i-0127a77bf7ac67eaf systemd[1]: Stopping User Manager for UID 1000...
Mar 28 06:12:45 i-0127a77bf7ac67eaf systemd[104663]: Stopped target Main User Target.
Mar 28 06:12:45 i-0127a77bf7ac67eaf systemd[104663]: Stopping PMS api server(fish)...

Reference

  1. https://github.com/systemd/systemd/issues/8486#issuecomment-374318982

[컴] jenkins 의 plugin list 를 추출하는 법

jenkins plugin 설치 / batch / cli 에서 설치 / 뽑기

jenkins 의 plugin list 를 추출하는 법

jenkins web page 에서 추출하기

https:///script 로 가서

Jenkins.instance.pluginManager.plugins.each{
  plugin -> 
    println ("${plugin.getShortName()}:${plugin.getVersion()}")
}

jenkins cli 로 뽑기

cli 는 다음처럼 받을 수 있다.

curl https:///jnlpJars/jenkins-cli.jar -o jenkins-cli.jar
// get_plugin_list.groovy
def plugins = jenkins.model.Jenkins.instance.getPluginManager().getPlugins()
plugins.each {println "${it.getShortName()}: ${it.getVersion()}"}
java -jar jenkins-cli.jar -s https:// -auth "namh:who1sthegoodman" groovy = < get_plugin_list.groovy >  plugins.txt

jenkins cli 로 plugin 설치

testing - How to install jenkins plugins from command line? - Stack Overflow

java -jar jenkins-cli.jar -s https://inhouse.foodpang.co/ci -auth <username>:<password> install-plugin <plugin_name1> <plugin_name2> ...

Reference

  1. How to get a list of installed Jenkins plugins with name and version pair - Stack Overflow

[컴][유틸] 2024-03, 에디터

hexa editor / hex editor / 괜찮은 hex editor

2024-03, 에디터

See Also

  1. 쿠…sal: [컴] sublime text 2 에서 binary 파일을 text 파일로 열기
  2. 쿠…sal: [컴][유틸] 대용량 파일 뷰어 big size file viewer

[컴] gradle java build 에서 git commit hash id 를 추가

스프링 / 스프링부트 / spring /

gradle java build 에서 git commit hash id 를 추가

com.gorylenko.gradle-git-properties gradle plugin 을 이용할 것이다.

build.gralde

아래처럼 build.gradle 을 설정하자. build 를 하면 build/resources/main/git.properties 가 만들어진다.

plugins {
  id "com.gorylenko.gradle-git-properties" version "2.4.1"
}
...
gitProperties {
    keys = ['git.branch','git.commit.id','git.commit.time']
    customProperty 'greeting', 'Hello' // expression
    // Customize file name (could be a file name or a relative file path below gitPropertiesResourceDir dir)
    gitPropertiesName = "my-git-file.properties"
    dotGitDirectory = file("${project.rootDir}/.git") # 정의하지 않으면, 자동으로 .git folder를 찾는다.
}

git.properties

위에서 gitPropertiesName 로 properties file 의 이름을 변경했기에, build/resources/main/my-git-file.properties 가 만들어진다.

my-git-file.properties 의 내용은 다음과 같다.

// build/resources/main/my-git-file.properties
git.branch=mybranch_name
git.commit.id=0c5da9f4f7312bb6c02f21c47ae129b31a006046
git.commit.time=2024-03-10T11\:18\:37+0900
greeting=Hello

jar 에서 위치

그리고 이것은 gradlew build 를 할 때 아래 경로에 들어가게 된다.

  • myjar-0.0.1.jar\BOOT-INF\classes\git.properties

SpringBoot

[컴] amazone 의 Correctto 17

 

amazone 의 Correctto 17

아마존에서 제공하는 OpenJDK 17 로 보면 된다. LTS(Long Term Support) 를 지원한다.

curl -LO https://corretto.aws/downloads/latest/amazon-corretto-17-x64-linux-jdk.tar.gz

아래 링크에 가면, deb, rpm 등도 다운로드 받을 수 있다.

[컴] MySql virtual column

mariadb 마리아db /

MySql virtual column

ALTER TABLE t1 ADD COLUMN c2 INT GENERATED ALWAYS AS (c1 + 1) STORED;
ALTER TABLE t1 ADD COLUMN c2 INT GENERATED ALWAYS AS (c1 + 1) VIRTUAL;

다른 table의 column을 이용하려면, view를 만드는 것도 방법이다.

Reference

  1. Generated (Virtual and Persistent/Stored) Columns - MariaDB Knowledge Base
  2. mysql - Stored/Virtual Generated Column- Pros/Cons/Best Practices? - Stack Overflow

[컴] SpringBoot 3.x 의 logging 설정

logback config / springboot log configuration / log config / spring log / 기간 설정

SpringBoot 3.x 의 logging 설정

spring-boot-starter-web을 사용한다면 logback 이 dependency 에 걸려있어서 자동으로 추가된다.

좀 더 자세한 logback option 의 설명

logback.rollingpolicy.max-history 에 대한 설명

TimeBasedRollingPolicy 를 사용한다면, max-history 는 ’며칠의 log를 보관하는 지를 결정’하는 설정이 된다고 한다.

totalSizeCap 을 설정해놓는 것이 disk usage 를 관리하는데 도움이 될 것이라고 한다.

[[컴] kafka app log 주기적으로 삭제

remove kakfka application log / kafka stream log / 자동 삭제

kafka app log 주기적으로 삭제

kafka log 라고 하면 대체로 data 에서 사용하는 log 로 검색된다. 여기선 kafka app 이 찍는 log 를 이야기 한다. 이것은 log4j 로 되어 있어서 log4j 설정을 잡아주면 된다. MaxFileSize, MaxBackupIndex 를 사용하면 된다.

  • MaxFileSize
  • MaxBackupIndex
log4j.rootLogger=INFO, stdout, kafkaAppender

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c)%n

log4j.appender.kafkaAppender=org.apache.log4j.RollingFileAppender
log4j.appender.kafkaAppender.File=${kafka.logs.dir}/server.log
log4j.appender.kafkaAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.kafkaAppender.layout.ConversionPattern=[%d] %p %m (%c)%n
log4j.appender.kafkaAppender.MaxFileSize=500MB
log4j.appender.kafkaAppender.MaxBackupIndex=5
...

DailyRollingFileAppender 에서 사용은 주의하자.

DailyRollingFileAppender 에서 MaxBackupIndex = 3 를 사용한다고 해도, 이것이 3일치 log를 보관하는 것을 이야기하는 것은 아니다. 같은 이름의 log에 대해서 backup 을 3개까지 보관가능하다는 뜻이다.

log.2009-11-07.1, log.2009-11-07.2, log.2009-11-07.3 을 보관한다는 이야기다.

Reference

  1. Kafka logs are not deleted - Ops - Confluent Community
  2. Logging · The Internals of Apache Kafka

[컴] 80열은 지키지 않아도 괜찮다.

80 cols / 80 columns /

80열은 지키지 않아도 괜찮다.

개인적으로도 여러가지 생각이 들었지만, 그래도 80열을 맞추는 것이 가독성을 더 좋게 할 것이라 생각했다. 예를 들면, 신문의 타이포그래피를 봐도 그렇지만, width를 일정수준으로 맞춰놓는다. 이것이 확실히 한눈에 한줄을 보기 쉽게하고, 빠르게 읽을 수 있게 해준다.

이런 장점이 있지만, code 는 그것과는 조금 다르게 보는 것이 맞을 듯 하다. 필자도 때론 한줄에 적는것이 더 좋을 것 같은 코드라인을 본다. 하지만 어떻게 하면 80을 맞출 수 있을까 생각한다.

나만 그러고 있는 듯 하지만, 더이상 80열을 심각하게 고려할 필요는 없을 듯 하다. 유명한 개발자가 한 이야기라 더 설득력이 있게 들린다. (물론 리누스는 리눅스 커널코드에 대한 이야기를 한 것이긴 하다.)

이것때문에 사내 좀 더 젊은 개발자에게 물어본 적이 있는데, 80열을 맞춰야 된다는 생각을 가진 분들은 없더라.^^;;;

간략하게 이야기하자면, 더이상 우리는 작은 터미널화면을 사용하던 시대에 살지 않으며, 더이상 그것을 고려할 이유가 없다고 이야기한다. 80열 터머널을 가진 유저는 wrapping 기능을 이용하면 그만이라고 설명한다.

끊임없이 변한다. 그래서 꾸준히 학습해야 한다. IT세상은 지루할 틈은 없어보인다.

Reference

  1. 80-characters-per-line limits should be terminal, says Linux kernel chief Linus Torvalds • The Register