Mac Catalina 업그레이드 후 루트 디렉토리를 사용할 수 없다. Read-Only 상태의 파티션으로 나누어져 있어 기존 루트 디렉토리의 사용자 디렉토리는 "/Users/Shared/Relocated Items/" 디렉토리로 모두 옮겨진다. 이는 디스크의 논리 파티션이 운영체제를 위한 ReadOnly 전용 공간과 사용자 데이터의 파티션으로 나뉘어지기 때문이다.

만약 SVN 을 루트 디렉토리로 사용한 경우 문제가 발생하는데, 적당한 디렉토리로 옮긴 후에 다음의 SVN 명령을 통해 URL 주소를 수정해 주어야 한다.

아래와 같이 현재 SVN 저장소의 정보를 보자

cd <svn directory>
svn info

그렇다면 아래와 유사한 결과가 출력된다.

Path: .
Working Copy Root Path: /Users/powerumc/...생략...
URL: file:///Users/powerumc/...생략...
Relative URL: ^/
Repository Root: file:///Users/powerumc/...생략...
Repository UUID: fe1381c0-03a0-ad4a-96c3-71fb4ed8e9fb
Revision: 18046
Node Kind: directory
Schedule: normal
Last Changed Author: SYSTEM
Last Changed Rev: 18046
Last Changed Date: 2018-11-21 15:43:56 +0900 (수, 21 11 2018)

위의 결과에서 URL 정보를 참고하여 변경된 URL 정보를 변경해 주면 된다.

svn relocate file:///Users/powerumc/repo/svn
Posted by 땡초 POWERUMC
TAG Catalina, Mac, svn

댓글을 달아 주세요

데이터 무결성이란

일반적으로 '데이터 무결성'이라고 함은 큰 범주에서 데이터베이스에서 데이터의 정확성과 일관성을 보증하는 것을 의미한다. 이런 데이터의 무결성을 보증할 수 없는 경우 우리는 '데이터가 변질되었다' 라고 할 수 있다. 이는 데이터가 우리가 기대하던 원본과 달라졌음을 의미한다.

일반적으로 파일이나 네트워크에서 무결성을 검증하기 위해 체크섬(checksum) 을 이용하고, 프로그래밍 언어에서는 해시값(hashvalue) 를 이용한다. 이 둘은 데이터의 무결성을 보장하기 위해 단 하나의 비트(bit) 의 데이터라도 수정이 되면 전체 해시값에 영향을 주어 원본과 일치하지 않는 해시값이 된다. 이 원본 해시값을 사본 해시값과 비교하면 데이터의 무결성이 보장되는지 쉽게 알 수 있다.

데이터와 관련된 소프트웨어 개발에서 무결성을 지키기란 쉽지 않다. 특히 여러 운영체제에서 동작하는 소프트웨어라면 운영체제의 특성과 관련된 부분으로 데이터 무결성이 쉽게 깨지곤 한다.

해시값을 통한 데이터 무결성

일반적으로 프로그래밍 언어에서의 데이터는 숫자형과 문자형이 있는데, 대부분 문자형의 데이터에서 데이터의 무결성이 깨지기 쉽다. 여러 운영체제에서 사용하는 개행 문자 값이 다르기 때문이다. 일반적 개발 툴에서는 개행 문자의 비트값은 우리 눈에 보이지 않는다.

  • 윈도우: CRLF (\r\n - &#A)
  • 맥: CR (\r - &#D)
  • 유닉스, 리눅스: LF (\n - &#A)

CR(Carriage Return) 은 0x0D 값이고,
LF(Line Feed) 는 0x0A 값이다.

이는 아주 간단한 실험으로 테스트해 볼 수 있다. 아래와 같이 일반적으로 "엔터키"를 누르면 추가되는 개행 문자 값은 모두 다른 것을 알 수 있다.

https://repl.it/@powerumc/string-carriage-return

일부 해시값을 계산하는 방법으로 공격하는 보안적인 취약점이 발견되어 일부 프로그래밍 언어의 특정 버전, 특정 플랫폼에서는 매번 해시값이 변한다.
예로 .NET Framework, .NET Core 와 Python 3.3 이상 버전부터는 새로운 프로세스가 실행되면 해시값도 항상 변하게 된다.

읽어볼거
Why is string.GetHashCode() different each time I run my program in .NET Core?https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/

using System;

class MainClass {
  public static void Main (string[] args) {
    var cr = "\r";
    var lf = "\n";
    var crlf = "\r\n";

    Console.WriteLine($"CR Hashcode={cr.GetHashCode()}");
    Console.WriteLine($"LF Hashcode={lf.GetHashCode()}");
    Console.WriteLine($"CRLF Hashcode={crlf.GetHashCode()}");

    Console.WriteLine($"cr = lf is {cr == lf}");
    Console.WriteLine($"cr = crlf is {cr == crlf}");
    Console.WriteLine($"lf = crlf is {cr == crlf}");
  }
}

// Results
// CR Hashcode=1948159545
// LF Hashcode=-1646523816
// CRLF Hashcode=-1196730459
// cr = lf is False
// cr = crlf is False
// lf = crlf is False

데이터 무결성이 깨지는 API

간단하게 테스트를 해볼 수 있는 다음의 XML 데이터를 다루는 소스코드를 준비했다. 원본 데이터의 개행 문자 값은 \r\n 이지만, 어떤 API 를 사용하느냐에 따라 반환되는 개행 문자 값은 달라진다. 만약 이런 API 들을 혼용해서 사용한다면 당연히 데이터의 무결성을 깨지게 된다.

  1. XmlTextReader 는 개행 문자 값 그대로 반환
  2. XmlReader\n 값으로 반환
  3. XmlDocument 는 개행 문자 값 그대로 반환
  4. XDocument \n 값으로 반환

https://repl.it/@powerumc/xml-carriage-return

데이터의 무결성이 깨지는 윈도우 클라이언트 프로그래밍

일반적으로 윈도우 클라이언트 프로그래밍을 할 여러 행의 문자열을 입력 받을 수 있는 컨트롤이 여기에 해당 된다. 이런 컨트롤은 내부적으로 Environment.NewLine 을 이용하는데, Environment.NewLine 자체가 운영체제에 해당하는 개행 문자 값을 반환한다.

예를 들어, 윈도우에서 구동되는 WPFTextBox 컨트롤이 개행 문자 값은 항상 \r\n 이 된다.

<TextBox AcceptsReturn="True"></TextBox>

데이터의 무결성이 깨지는 웹 프로그래밍

웹에서는 또 어떨까? 일반적으로 개행 문자를 입력 받을 수 있는 TextArea 의 개행 문자 값은 \n 이다. 이는 아래의 테스트 코드에서 확인해 볼 수 있다.

show 버튼을 클릭하면 자바스크립트로 개행 문자를 텍스트로 표시해 주도록 했고, submit 버튼을 클릭하면 서버로 폼의 데이터가 전송되도록 했다. 여기에서 눈여겨 보아야 할 것이 있는데 클라이언트의 결과와 서버로 전송된 데이터는 개행 문자가 달라진다.

  • HTML TextArea 컨트롤은 개행 문자를 \n 을 사용한다
  • Form 전송 시 기본 값인 enctype="application/x-www-form-urlencoded" 인 경우 개행문자는 \n 로 치환된다

Form 전송 시 Request Header 정보는 아래와 같다.

:method: POST
:path: /submit
:scheme: https
content-type: application/x-www-form-urlencoded

아래는 Form 전송 시 URL Encoded 된 Form Data 값이다. %0D%0A 값에서 알 수 있듯이 \n 개행 문자가 \r\n 로 치환된 것을 알 수 있다.

txt: Hello%0D%0AWorld

https://repl.it/@powerumc/html-textarea-carriage-return-by-node

클라이언트 HTML 코드 (index.html)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>html textarea carriage return test</title>
  </head>
  <body>
    <form action="/submit" method="POST">
      <textarea id="txt" name="txt" style="height: 200px; width: 200px;"></textarea>
      <button id="btn" type="button">show</button>
      <span id="span"></span>
      <button type="submit">submit</button>
    </form>
  </body>

  <script>
    document.querySelector("#btn").addEventListener("click",
      function() {
        var text = document.querySelector("#txt").value
          .replace(/\r/g, "\\r")
          .replace(/\n/g, "\\n");

        document.querySelector("#span").innerText = text;
      });
  </script>
  </body>
</html>

서버 자바스크립트 코드 (index.js)

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

app.use(express.static('public'));

app.get('/', (req, res) => {
  res.sendFile('public/index.html');
});

app.post("/submit", (req, res) => {
  var txt = req.body.txt;
  res.send(txt.replace(/\r/g, "\\r")
          .replace(/\n/g, "\\n"));
});

app.listen(3000, () => console.log('server started'));

읽어볼거리
HTML textarea의 개행문자는 무엇일까? (LF vs CRLF vs 상황에 따라 다르다 vs 충격과 공포)
https://libsora.so/posts/what-is-textarea-newline/

마무리

일반적인 서비스/비즈니스 개발에서 개행 문자로 인해 해시값이 달라지는 문제는 크게 의미가 없을 수 있다. 그러나 다양한 운영체제를 지원하는 크로스 플랫폼에서는 문제가 될 수 있다. 사용자에게 보이는 화면의 텍스트의 한 줄의 빈 공백이 두 줄이 되는 경우가 있고, 데이터를 파일과 같은 저장소에 저장하는 경우 개행 문자가 달라지는 경우도 발생한다. 모바일 게임에서 이 개행 문자 하나로 해시 값이 달라져 데이터 파일을 1GB 를 다운로드 받는다고 생각하면 정말 끔찍한 일이다.

윈도우 클라이언트에서, 모바일 기기에서, 웹 페이지에서, 다양한 운영체제의 클라이언트에서 입력한 같은 데이터를 프로그래밍 언어는 다르다고 해석할 수 있다. 이것이 비즈니스에 영향을 줄 수 있다면 올바로 바로잡는 것도 좋을 것이다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요

개요

mono-service 는 .NET Framework 로 작성된 윈도우 서비스(Windows Services)mono 환경에서 구동할 수 있는 도구이다. 윈도우 서비스는 일반적으로 GUI 가 없는 백그라운드로 동작하는 실행 바이너리로 윈도우 운영체제가 서비스를 안정적으로 동작하도록 지원해 준다.

mono-service 는 .NET Framework 로 컴파일된 바이너리 및 실행 파일을 AppDomain 을 생성한 후 로드한다. mono-service 가 하는 역할은 일반적으로 POSIX 가 정의하는 유닉스 시그널(Unix Signals) 를 받아 처리하기 위한 용도이다. 맥 또는 리눅스 운영체제는 윈도우 운영체제가 제공하는 서비스의 시작/중지 명령을 이해할 수 없기 때문에 SIGINT, SIGKILL 신호 등을 받아서 서비스를 중지하도록 해야 한다.

필자가 회사에서 담당하고 있는 오픈소스 프로젝트인 크레마(게임 데이터 개발 도구)는 전반적으로 플러그인 아키텍처로 여러 가지 기능을 제공한다. 크레마 서버는 다양한 운영체제에서 동작이 가능하도록 mono 에서 실행할 수 있는데, 맥의 launchctl 과 리눅스의 systemd 로 서비스를 제공하기 위해 mono-service 로 서비스 바이너리 파일을 호스팅하도록 한다.

문제 발생

일반적으로 .NET Framework 에서 AppDomain.CurrentDomain.BaseDirectory 속성의 반환되는 결과는 경로 마지막에 / 문자열 붙여준다. 반면 Environment.CurrentDirectory 속성은 마지막에 / 문자열을 붙이지 않는다.

간단한 아래의 콘솔 프로그램의 결과를 보면 쉽게 알 수 있다.

using System;

class MainClass {
  public static void Main (string[] args) {
    Console.WriteLine (AppDomain.CurrentDomain.BaseDirectory);
    Console.WriteLine (Environment.CurrentDirectory);
  }
}

// Results
// ...생략.../bin/debug/
// ...생략.../bin/debug

Path.GetDirectoryName와 조합하면 기대하지 않은 결과가 나올 수 있다. Path.GetDirectoryName/ 가 없는 경로의 마지막은 파일로 인식하여 그 부모의 디렉토리 이름까지의 경로를 반환하는데에서 발생한다.

Console.WriteLine (AppDomain.CurrentDomain.BaseDirectory);
Console.WriteLine (Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory));

Console.WriteLine (Environment.CurrentDirectory);
Console.WriteLine (Path.GetDirectoryName(Environment.CurrentDirectory));

// Results
// ...생략.../bin/debug/
// ...생략.../bin/debug
// ...생략.../bin/debug
// ...생략.../bin/

https://repl.it/@powerumc/BaseDirectory-vs-EnvironmentCurrentDirectory

mono-service 버그

처음 언급한 것처럼 mono-serviceAppDomain 을 생성하여 서비스를 실행한 바이너리를 로드한다. 여기에서 AppDomainApplicationBase 디렉토리를 Environment.CurrentDirectory 로 설정하는 바람에 서비스로 실행되는 컨텍스트에서 AppDomain.CurrentDomain.BaseDirectory 값이 기대한 값과 다르게 반환된다.

아래의 링크는 이런 문제를 해결하기 위해 Pull Request 를 보냈고, 정상적으로 메인 저장소에 머지가 되었다.

Fixed a bug in mono-service.cs by powerumc · Pull Request #17095 · mono/mono

아래의 코드는 간단하게 만든 mono-service 에서 구동할 예제 코드이다.

using System;
using System.ComponentModel;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;

namespace MonoServiceTest
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceBase.Run(new MonoServiceTest());
        }
    }

    public class MonoServiceTest : ServiceBase
    {
        protected override void OnStart(string[] args)
        {
            // Debugger.Launch();
            var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            var directoryPath = Path.GetDirectoryName(baseDirectory);
            var logFilePath = Path.Combine(directoryPath, "log.txt");
            Console.WriteLine($"AppDomain.BaseDirectory = {baseDirectory}");
            Console.WriteLine($"DirectoryPath = {directoryPath}");
            Console.WriteLine($"LogFile Path = {logFilePath}");
        }
    }

    [RunInstaller(true)]
    public class MonoServiceInstaller : Installer
    {
        public MonoServiceInstaller()
        {
            this.Installers.AddRange(new Installer[]
            {
                new ServiceProcessInstaller
                {
                    Username = null,
                    Password = null,
                    Account = ServiceAccount.LocalSystem
                },
                new ServiceInstaller
                {
                    DisplayName = nameof(MonoServiceTest),
                    ServiceName = nameof(MonoServiceTest),
                    StartType = ServiceStartMode.Automatic
                }
            });
        }
    }
}

위의 코드를 컴파일 한 후 mono-service 로 실행하면 아래와 같이 잘못된 결과를 반환하는 것을 알 수 있다.

mono-service --no-daemon mono_service_test.exe

// Results
// AppDomain.BaseDirectory = ...생략.../bin/Debug
// DirectoryPath = ...생략.../bin
// LogFile Path = ...생략.../bin/log.txt

마무리

크로스 플랫폼을 지원하기 위해 기존 레거시를 .NET Core 로 전환하기엔 기술적인 부분과 운영적인 이슈도 있어서 mono-service 를 검토해 보았다. 그 중 기술적인 부분으로는 더 이상 WCF 서버는 .NET Core 에서 지원하지 않고, gRPC 사용을 권장하고 있다. 이를 위해 gRPC 를 WCF 와 대응되도록 호환 레이어를 만들어 전환하기 위해서 수 많은 테스트를 해야 하고 또 장애에 대응해야 한다.

기존 코드를 이식성이 좋은 mono 를 통해 여러 운영체제를 지원하고자 하였지만, mono-service 의 버그로 인해 버그를 조사하고 PR 를 보내기까지 많은 시간이 소요되었다.

위 버그 픽스 코드가 바로 릴리즈 되는 것은 아니기에 우선적으로 AppDomain.BaseDirectory 의 기대하지 않은 경로에 대해서도 올바르게 동작하도록 개발 중인 소스 코드를 수정해야 했다. 아마 차기 mono 릴리즈 버전에서는 이 문제가 수정될 것이니 가능하면 최신 버전의 mono 를 유지하는 것이 좋을 것 같다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요